@canonry/canonry 4.117.1 → 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 +74 -1
- package/assets/assets/AuditHistoryPanel-Yh7VEe4P.js +1 -0
- package/assets/assets/{BacklinksPage-Bkafm10m.js → BacklinksPage-BmO77kMs.js} +1 -1
- package/assets/assets/{ChartPrimitives-NV_GgqFn.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-df0d24S6.js → RunRow-BDH9agxQ.js} +1 -1
- package/assets/assets/RunsPage-D9YsEeT3.js +1 -0
- package/assets/assets/{SettingsPage-BKw6B6cd.js → SettingsPage-o5as3Ixw.js} +1 -1
- package/assets/assets/{TrafficPage-CzpiYHKj.js → TrafficPage-BCK203aQ.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CC5nuqM1.js → TrafficSourceDetailPage-DsVMAZvS.js} +1 -1
- package/assets/assets/{arrow-left-CbQcW2pL.js → arrow-left-B7ZR6VC9.js} +1 -1
- package/assets/assets/{extract-error-message-CvqIZ-le.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-3J2u0woR.js → trash-2-xxHTqWbF.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-BMBHUFMI.js → chunk-75UBTCKS.js} +540 -16
- package/dist/{chunk-V4SW7SVX.js → chunk-CMZIPFNQ.js} +1598 -486
- package/dist/{chunk-NY7SC4ES.js → chunk-HGNY5GL4.js} +384 -15
- package/dist/{chunk-IAJ5TQ4S.js → chunk-KEZWLP4O.js} +1385 -1145
- package/dist/cli.js +390 -113
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-C5YLASIP.js → intelligence-service-FWPXMAVK.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
- package/assets/assets/ProjectPage-0-NnRxys.js +0 -7
- package/assets/assets/RunsPage-CU6-4YTM.js +0 -1
- package/assets/assets/index-B4R1-8hR.css +0 -1
- package/assets/assets/index-LjcWHaNZ.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,
|
|
@@ -220,11 +236,14 @@ import {
|
|
|
220
236
|
resolveConfigSpecQueries,
|
|
221
237
|
resolveLocations,
|
|
222
238
|
resolveSnapshotRequestQueries,
|
|
239
|
+
resultsExportDtoSchema,
|
|
223
240
|
runDetailDtoSchema,
|
|
224
241
|
runDtoSchema,
|
|
225
242
|
runInProgress,
|
|
226
243
|
runNotCancellable,
|
|
244
|
+
runStatusSchema,
|
|
227
245
|
runTriggerRequestSchema,
|
|
246
|
+
runTriggerSchema,
|
|
228
247
|
runtimeStateMissing,
|
|
229
248
|
schedulableRunKindSchema,
|
|
230
249
|
scheduleDtoSchema,
|
|
@@ -280,10 +299,10 @@ import {
|
|
|
280
299
|
wordpressSchemaDeployResultDtoSchema,
|
|
281
300
|
wordpressSchemaStatusResultDtoSchema,
|
|
282
301
|
wordpressStatusDtoSchema
|
|
283
|
-
} from "./chunk-
|
|
302
|
+
} from "./chunk-KEZWLP4O.js";
|
|
284
303
|
|
|
285
304
|
// src/intelligence-service.ts
|
|
286
|
-
import { eq 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";
|
|
287
306
|
|
|
288
307
|
// ../db/src/client.ts
|
|
289
308
|
import { mkdirSync } from "fs";
|
|
@@ -299,6 +318,7 @@ __export(schema_exports, {
|
|
|
299
318
|
adsCampaigns: () => adsCampaigns,
|
|
300
319
|
adsConnections: () => adsConnections,
|
|
301
320
|
adsInsightsDaily: () => adsInsightsDaily,
|
|
321
|
+
adsOperations: () => adsOperations,
|
|
302
322
|
agentMemory: () => agentMemory,
|
|
303
323
|
agentSessions: () => agentSessions,
|
|
304
324
|
agentToolEvents: () => agentToolEvents,
|
|
@@ -1441,11 +1461,33 @@ var adsConnections = sqliteTable("ads_connections", {
|
|
|
1441
1461
|
}, (table) => [
|
|
1442
1462
|
uniqueIndex("idx_ads_conn_project").on(table.projectId)
|
|
1443
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
|
+
]);
|
|
1444
1483
|
var adsCampaigns = sqliteTable("ads_campaigns", {
|
|
1445
1484
|
id: text("id").primaryKey(),
|
|
1446
1485
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1447
1486
|
name: text("name").notNull(),
|
|
1487
|
+
description: text("description"),
|
|
1448
1488
|
status: text("status").notNull(),
|
|
1489
|
+
startTime: integer("start_time"),
|
|
1490
|
+
endTime: integer("end_time"),
|
|
1449
1491
|
biddingType: text("bidding_type"),
|
|
1450
1492
|
dailySpendLimitMicros: integer("daily_spend_limit_micros"),
|
|
1451
1493
|
lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
|
|
@@ -1462,6 +1504,7 @@ var adsAdGroups = sqliteTable("ads_ad_groups", {
|
|
|
1462
1504
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1463
1505
|
campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
|
|
1464
1506
|
name: text("name").notNull(),
|
|
1507
|
+
description: text("description"),
|
|
1465
1508
|
status: text("status").notNull(),
|
|
1466
1509
|
billingEventType: text("billing_event_type"),
|
|
1467
1510
|
maxBidMicros: integer("max_bid_micros"),
|
|
@@ -3597,6 +3640,34 @@ var MIGRATION_VERSIONS = [
|
|
|
3597
3640
|
run: (tx) => {
|
|
3598
3641
|
relinkOrphanedSnapshotQueryIds(tx);
|
|
3599
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
|
+
]
|
|
3600
3671
|
}
|
|
3601
3672
|
];
|
|
3602
3673
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -5945,6 +6016,14 @@ import fs8 from "fs";
|
|
|
5945
6016
|
import crypto2 from "crypto";
|
|
5946
6017
|
import { eq } from "drizzle-orm";
|
|
5947
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
|
+
}
|
|
5948
6027
|
function requireScope(request, scope) {
|
|
5949
6028
|
const key = request.apiKey;
|
|
5950
6029
|
if (!key) return;
|
|
@@ -6097,6 +6176,9 @@ async function authPlugin(app, opts = {}) {
|
|
|
6097
6176
|
if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
|
|
6098
6177
|
throw forbidden("This API key is read-only and cannot perform write operations.");
|
|
6099
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
|
+
}
|
|
6100
6182
|
enforceEmbedProjectTabs(request, opts.embedProjectTabs);
|
|
6101
6183
|
if (key.projectId) {
|
|
6102
6184
|
const match = url.match(/\/projects\/([^/?#]+)/);
|
|
@@ -7991,7 +8073,7 @@ function normalizeCompetitorList2(domains) {
|
|
|
7991
8073
|
}
|
|
7992
8074
|
|
|
7993
8075
|
// ../api-routes/src/history.ts
|
|
7994
|
-
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";
|
|
7995
8077
|
|
|
7996
8078
|
// ../api-routes/src/notification-redaction.ts
|
|
7997
8079
|
var REDACTED_URL = {
|
|
@@ -8037,12 +8119,16 @@ function redactNotificationDiff(value) {
|
|
|
8037
8119
|
async function historyRoutes(app) {
|
|
8038
8120
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
8039
8121
|
const project = resolveProject(app.db, request.params.name);
|
|
8040
|
-
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();
|
|
8041
8125
|
return reply.send(rows.map(formatAuditEntry));
|
|
8042
8126
|
});
|
|
8043
8127
|
app.get("/history", async (request, reply) => {
|
|
8044
8128
|
const scopedProjectId = request.apiKey?.projectId;
|
|
8045
|
-
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();
|
|
8046
8132
|
return reply.send(rows.map(formatAuditEntry));
|
|
8047
8133
|
});
|
|
8048
8134
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -8098,7 +8184,8 @@ async function historyRoutes(app) {
|
|
|
8098
8184
|
app.get("/projects/:name/timeline", async (request, reply) => {
|
|
8099
8185
|
const project = resolveProject(app.db, request.params.name);
|
|
8100
8186
|
const projectQueries = app.db.select().from(queries).where(eq10(queries.projectId, project.id)).all();
|
|
8101
|
-
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();
|
|
8102
8189
|
if (projectRuns.length === 0 || projectQueries.length === 0) {
|
|
8103
8190
|
return reply.send([]);
|
|
8104
8191
|
}
|
|
@@ -8248,29 +8335,23 @@ async function historyRoutes(app) {
|
|
|
8248
8335
|
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run2)).all();
|
|
8249
8336
|
const map1 = /* @__PURE__ */ new Map();
|
|
8250
8337
|
for (const s of snaps1) {
|
|
8251
|
-
const resolved = {
|
|
8252
|
-
...s,
|
|
8253
|
-
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
8254
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
8255
|
-
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
8256
|
-
};
|
|
8257
8338
|
const existing = map1.get(s.queryId);
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
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
|
+
});
|
|
8261
8345
|
}
|
|
8262
8346
|
const map2 = /* @__PURE__ */ new Map();
|
|
8263
8347
|
for (const s of snaps2) {
|
|
8264
|
-
const resolved = {
|
|
8265
|
-
...s,
|
|
8266
|
-
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
8267
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
8268
|
-
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
8269
|
-
};
|
|
8270
8348
|
const existing = map2.get(s.queryId);
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
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
|
+
});
|
|
8274
8355
|
}
|
|
8275
8356
|
const allQueryIds = /* @__PURE__ */ new Set([...map1.keys(), ...map2.keys()]);
|
|
8276
8357
|
const diff = [...allQueryIds].map((qId) => {
|
|
@@ -8284,10 +8365,10 @@ async function historyRoutes(app) {
|
|
|
8284
8365
|
run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
|
|
8285
8366
|
run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
|
|
8286
8367
|
// Legacy aliases — same data as run{1,2}MentionState below.
|
|
8287
|
-
run1VisibilityState: s1
|
|
8288
|
-
run2VisibilityState: s2
|
|
8289
|
-
run1MentionState: s1
|
|
8290
|
-
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,
|
|
8291
8372
|
changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
|
|
8292
8373
|
visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
|
|
8293
8374
|
};
|
|
@@ -8304,9 +8385,28 @@ function formatAuditEntry(row) {
|
|
|
8304
8385
|
entityType: row.entityType,
|
|
8305
8386
|
entityId: row.entityId,
|
|
8306
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,
|
|
8307
8390
|
createdAt: row.createdAt
|
|
8308
8391
|
};
|
|
8309
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
|
+
}
|
|
8310
8410
|
|
|
8311
8411
|
// ../api-routes/src/analytics.ts
|
|
8312
8412
|
import { and as and7, eq as eq11, desc as desc3, inArray as inArray4 } from "drizzle-orm";
|
|
@@ -8970,7 +9070,7 @@ async function intelligenceRoutes(app) {
|
|
|
8970
9070
|
}
|
|
8971
9071
|
|
|
8972
9072
|
// ../api-routes/src/report.ts
|
|
8973
|
-
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";
|
|
8974
9074
|
|
|
8975
9075
|
// ../api-routes/src/content.ts
|
|
8976
9076
|
import crypto12 from "crypto";
|
|
@@ -9706,7 +9806,7 @@ function winnabilityClassRank(winnabilityClass) {
|
|
|
9706
9806
|
}
|
|
9707
9807
|
|
|
9708
9808
|
// ../api-routes/src/gsc-totals.ts
|
|
9709
|
-
import { and as and11, asc as
|
|
9809
|
+
import { and as and11, asc as asc3, eq as eq15, sql as sql6 } from "drizzle-orm";
|
|
9710
9810
|
function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
9711
9811
|
const rows = db.select({
|
|
9712
9812
|
date: gscDailyTotals.date,
|
|
@@ -9719,7 +9819,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
|
9719
9819
|
sql6`${gscDailyTotals.date} >= ${startDate}`,
|
|
9720
9820
|
sql6`${gscDailyTotals.date} <= ${endDate}`
|
|
9721
9821
|
)
|
|
9722
|
-
).orderBy(
|
|
9822
|
+
).orderBy(asc3(gscDailyTotals.date)).all();
|
|
9723
9823
|
return rows.map((r) => {
|
|
9724
9824
|
const position = Number(r.position);
|
|
9725
9825
|
return {
|
|
@@ -12186,7 +12286,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12186
12286
|
eq16(gaAiReferrals.sourceDimension, "session")
|
|
12187
12287
|
];
|
|
12188
12288
|
if (snapshotStartDate && snapshotMaxDate) {
|
|
12189
|
-
aiConditions.push(
|
|
12289
|
+
aiConditions.push(gte3(gaAiReferrals.date, snapshotStartDate));
|
|
12190
12290
|
aiConditions.push(lte(gaAiReferrals.date, snapshotMaxDate));
|
|
12191
12291
|
}
|
|
12192
12292
|
const aiSessionRows = db.select({
|
|
@@ -12403,7 +12503,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12403
12503
|
and12(
|
|
12404
12504
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12405
12505
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12406
|
-
|
|
12506
|
+
gte3(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12407
12507
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
12408
12508
|
)
|
|
12409
12509
|
).get()?.total ?? 0
|
|
@@ -12413,7 +12513,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12413
12513
|
and12(
|
|
12414
12514
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12415
12515
|
ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12416
|
-
|
|
12516
|
+
gte3(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12417
12517
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
12418
12518
|
)
|
|
12419
12519
|
).get()?.total ?? 0
|
|
@@ -12427,7 +12527,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12427
12527
|
and12(
|
|
12428
12528
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12429
12529
|
nonSubresourceReferralPathCondition(),
|
|
12430
|
-
|
|
12530
|
+
gte3(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
12431
12531
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
12432
12532
|
)
|
|
12433
12533
|
).get();
|
|
@@ -12437,7 +12537,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12437
12537
|
db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
12438
12538
|
and12(
|
|
12439
12539
|
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12440
|
-
|
|
12540
|
+
gte3(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
12441
12541
|
exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
12442
12542
|
)
|
|
12443
12543
|
).get()?.total ?? 0
|
|
@@ -12457,7 +12557,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12457
12557
|
}).from(crawlerEventsHourly).where(
|
|
12458
12558
|
and12(
|
|
12459
12559
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12460
|
-
|
|
12560
|
+
gte3(crawlerEventsHourly.tsHour, headlineStart),
|
|
12461
12561
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12462
12562
|
)
|
|
12463
12563
|
).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
|
|
@@ -12468,7 +12568,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12468
12568
|
and12(
|
|
12469
12569
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12470
12570
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12471
|
-
|
|
12571
|
+
gte3(crawlerEventsHourly.tsHour, priorStart),
|
|
12472
12572
|
lt(crawlerEventsHourly.tsHour, headlineStart)
|
|
12473
12573
|
)
|
|
12474
12574
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
@@ -12479,7 +12579,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12479
12579
|
and12(
|
|
12480
12580
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12481
12581
|
nonSubresourceReferralPathCondition(),
|
|
12482
|
-
|
|
12582
|
+
gte3(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12483
12583
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12484
12584
|
)
|
|
12485
12585
|
).groupBy(aiReferralEventsHourly.operator).all();
|
|
@@ -12489,7 +12589,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12489
12589
|
}).from(aiUserFetchEventsHourly).where(
|
|
12490
12590
|
and12(
|
|
12491
12591
|
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12492
|
-
|
|
12592
|
+
gte3(aiUserFetchEventsHourly.tsHour, headlineStart),
|
|
12493
12593
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12494
12594
|
)
|
|
12495
12595
|
).groupBy(aiUserFetchEventsHourly.operator).all();
|
|
@@ -12534,7 +12634,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12534
12634
|
and12(
|
|
12535
12635
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12536
12636
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12537
|
-
|
|
12637
|
+
gte3(crawlerEventsHourly.tsHour, headlineStart),
|
|
12538
12638
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12539
12639
|
)
|
|
12540
12640
|
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
@@ -12551,7 +12651,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12551
12651
|
and12(
|
|
12552
12652
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12553
12653
|
nonSubresourceReferralPathCondition(),
|
|
12554
|
-
|
|
12654
|
+
gte3(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12555
12655
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12556
12656
|
)
|
|
12557
12657
|
).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
|
|
@@ -12568,7 +12668,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12568
12668
|
and12(
|
|
12569
12669
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12570
12670
|
nonSubresourceReferralPathCondition(),
|
|
12571
|
-
|
|
12671
|
+
gte3(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12572
12672
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12573
12673
|
)
|
|
12574
12674
|
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
@@ -12584,7 +12684,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12584
12684
|
and12(
|
|
12585
12685
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12586
12686
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12587
|
-
|
|
12687
|
+
gte3(crawlerEventsHourly.tsHour, trendStart),
|
|
12588
12688
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12589
12689
|
)
|
|
12590
12690
|
).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
|
|
@@ -12595,7 +12695,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12595
12695
|
and12(
|
|
12596
12696
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12597
12697
|
nonSubresourceReferralPathCondition(),
|
|
12598
|
-
|
|
12698
|
+
gte3(aiReferralEventsHourly.tsHour, trendStart),
|
|
12599
12699
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12600
12700
|
)
|
|
12601
12701
|
).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
|
|
@@ -12605,7 +12705,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12605
12705
|
}).from(aiUserFetchEventsHourly).where(
|
|
12606
12706
|
and12(
|
|
12607
12707
|
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12608
|
-
|
|
12708
|
+
gte3(aiUserFetchEventsHourly.tsHour, trendStart),
|
|
12609
12709
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12610
12710
|
)
|
|
12611
12711
|
).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
|
|
@@ -14239,8 +14339,226 @@ async function visibilityStatsRoutes(app) {
|
|
|
14239
14339
|
});
|
|
14240
14340
|
}
|
|
14241
14341
|
|
|
14342
|
+
// ../api-routes/src/results-export.ts
|
|
14343
|
+
import { and as and15, asc as asc4, eq as eq19, gte as gte4, lte as lte2 } from "drizzle-orm";
|
|
14344
|
+
var CSV_COLUMNS = [
|
|
14345
|
+
"export_schema_version",
|
|
14346
|
+
"project_id",
|
|
14347
|
+
"project_name",
|
|
14348
|
+
"project_display_name",
|
|
14349
|
+
"canonical_domain",
|
|
14350
|
+
"run_id",
|
|
14351
|
+
"run_kind",
|
|
14352
|
+
"run_status",
|
|
14353
|
+
"run_trigger",
|
|
14354
|
+
"run_created_at",
|
|
14355
|
+
"run_started_at",
|
|
14356
|
+
"run_finished_at",
|
|
14357
|
+
"snapshot_id",
|
|
14358
|
+
"snapshot_created_at",
|
|
14359
|
+
"query_id",
|
|
14360
|
+
"query",
|
|
14361
|
+
"provider",
|
|
14362
|
+
"model",
|
|
14363
|
+
"location",
|
|
14364
|
+
"citation_state",
|
|
14365
|
+
"cited",
|
|
14366
|
+
"answer_mentioned",
|
|
14367
|
+
"mention_state",
|
|
14368
|
+
"cited_domains_json",
|
|
14369
|
+
"competitor_overlap_json",
|
|
14370
|
+
"recommended_competitors_json",
|
|
14371
|
+
"answer_text",
|
|
14372
|
+
"grounding_sources_json",
|
|
14373
|
+
"search_queries_json"
|
|
14374
|
+
];
|
|
14375
|
+
function parseFormat(value) {
|
|
14376
|
+
if (value === void 0 || value === "" || value === "json") return "json";
|
|
14377
|
+
if (value === "csv") return "csv";
|
|
14378
|
+
throw validationError('"format" must be "json" or "csv"');
|
|
14379
|
+
}
|
|
14380
|
+
function parseBoolean(value, name) {
|
|
14381
|
+
if (value === void 0 || value === "") return false;
|
|
14382
|
+
if (value === "true" || value === "1") return true;
|
|
14383
|
+
if (value === "false" || value === "0") return false;
|
|
14384
|
+
throw validationError(`"${name}" must be "true" or "false"`);
|
|
14385
|
+
}
|
|
14386
|
+
function parseDateBounds(since, until) {
|
|
14387
|
+
let sinceIso = null;
|
|
14388
|
+
let untilIso = null;
|
|
14389
|
+
if (since !== void 0 && since !== "") {
|
|
14390
|
+
const ms = Date.parse(since);
|
|
14391
|
+
if (Number.isNaN(ms)) throw validationError('"since" must be an ISO 8601 date/time');
|
|
14392
|
+
sinceIso = new Date(ms).toISOString();
|
|
14393
|
+
}
|
|
14394
|
+
if (until !== void 0 && until !== "") {
|
|
14395
|
+
const ms = parseInclusiveEndMs(until);
|
|
14396
|
+
if (ms === null) throw validationError('"until" must be an ISO 8601 date/time');
|
|
14397
|
+
untilIso = new Date(ms).toISOString();
|
|
14398
|
+
}
|
|
14399
|
+
if (sinceIso !== null && untilIso !== null && untilIso < sinceIso) {
|
|
14400
|
+
throw validationError('"until" must be on or after "since"');
|
|
14401
|
+
}
|
|
14402
|
+
return { sinceIso, untilIso };
|
|
14403
|
+
}
|
|
14404
|
+
function readEvidence(rawResponse) {
|
|
14405
|
+
const parsed = parseJsonColumn(rawResponse, {});
|
|
14406
|
+
const groundingSources = Array.isArray(parsed.groundingSources) ? parsed.groundingSources.filter(
|
|
14407
|
+
(source) => typeof source === "object" && source !== null && typeof source.uri === "string"
|
|
14408
|
+
).map((source) => ({ uri: source.uri, title: typeof source.title === "string" ? source.title : "" })) : [];
|
|
14409
|
+
const searchQueries = Array.isArray(parsed.searchQueries) ? parsed.searchQueries.filter((query) => typeof query === "string") : [];
|
|
14410
|
+
return { groundingSources, searchQueries };
|
|
14411
|
+
}
|
|
14412
|
+
function recordMentionState(answerMentioned) {
|
|
14413
|
+
if (answerMentioned === null) return null;
|
|
14414
|
+
return answerMentioned ? "mentioned" : "not-mentioned";
|
|
14415
|
+
}
|
|
14416
|
+
function spreadsheetSafe(value) {
|
|
14417
|
+
return /^[\t\r ]*[=+\-@]/.test(value) ? `'${value}` : value;
|
|
14418
|
+
}
|
|
14419
|
+
function csvCell(value) {
|
|
14420
|
+
if (value === null) return "";
|
|
14421
|
+
const text2 = typeof value === "string" ? spreadsheetSafe(value) : String(value);
|
|
14422
|
+
return /[",\r\n]/.test(text2) ? `"${text2.replace(/"/g, '""')}"` : text2;
|
|
14423
|
+
}
|
|
14424
|
+
function renderResultsExportCsv(report) {
|
|
14425
|
+
const rows = report.records.map((record) => [
|
|
14426
|
+
report.schemaVersion,
|
|
14427
|
+
report.project.id,
|
|
14428
|
+
report.project.name,
|
|
14429
|
+
report.project.displayName,
|
|
14430
|
+
report.project.canonicalDomain,
|
|
14431
|
+
record.runId,
|
|
14432
|
+
record.runKind,
|
|
14433
|
+
record.runStatus,
|
|
14434
|
+
record.runTrigger,
|
|
14435
|
+
record.runCreatedAt,
|
|
14436
|
+
record.runStartedAt,
|
|
14437
|
+
record.runFinishedAt,
|
|
14438
|
+
record.snapshotId,
|
|
14439
|
+
record.snapshotCreatedAt,
|
|
14440
|
+
record.queryId,
|
|
14441
|
+
record.query,
|
|
14442
|
+
record.provider,
|
|
14443
|
+
record.model,
|
|
14444
|
+
record.location,
|
|
14445
|
+
record.citationState,
|
|
14446
|
+
record.cited,
|
|
14447
|
+
record.answerMentioned,
|
|
14448
|
+
record.mentionState,
|
|
14449
|
+
JSON.stringify(record.citedDomains),
|
|
14450
|
+
JSON.stringify(record.competitorOverlap),
|
|
14451
|
+
JSON.stringify(record.recommendedCompetitors),
|
|
14452
|
+
record.answerText,
|
|
14453
|
+
JSON.stringify(record.groundingSources),
|
|
14454
|
+
JSON.stringify(record.searchQueries)
|
|
14455
|
+
].map(csvCell).join(","));
|
|
14456
|
+
return [CSV_COLUMNS.join(","), ...rows].join("\r\n") + "\r\n";
|
|
14457
|
+
}
|
|
14458
|
+
function filenameFor(projectName, format) {
|
|
14459
|
+
const safeName = projectName.replace(/[^\w.-]+/gi, "_") || "project";
|
|
14460
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
14461
|
+
return `canonry-results-${safeName}-${date}.${format}`;
|
|
14462
|
+
}
|
|
14463
|
+
async function resultsExportRoutes(app) {
|
|
14464
|
+
app.get("/projects/:name/results/export", async (request, reply) => {
|
|
14465
|
+
const project = resolveProject(app.db, request.params.name);
|
|
14466
|
+
const format = parseFormat(request.query.format);
|
|
14467
|
+
const includeProbes = parseBoolean(request.query.includeProbes, "includeProbes");
|
|
14468
|
+
const { sinceIso, untilIso } = parseDateBounds(request.query.since, request.query.until);
|
|
14469
|
+
const conditions = [
|
|
14470
|
+
eq19(runs.projectId, project.id),
|
|
14471
|
+
eq19(runs.kind, RunKinds["answer-visibility"]),
|
|
14472
|
+
...includeProbes ? [] : [notProbeRun()],
|
|
14473
|
+
...sinceIso ? [gte4(runs.createdAt, sinceIso)] : [],
|
|
14474
|
+
...untilIso ? [lte2(runs.createdAt, untilIso)] : []
|
|
14475
|
+
];
|
|
14476
|
+
const rows = app.db.select({
|
|
14477
|
+
runId: runs.id,
|
|
14478
|
+
runStatus: runs.status,
|
|
14479
|
+
runTrigger: runs.trigger,
|
|
14480
|
+
runCreatedAt: runs.createdAt,
|
|
14481
|
+
runStartedAt: runs.startedAt,
|
|
14482
|
+
runFinishedAt: runs.finishedAt,
|
|
14483
|
+
snapshotId: querySnapshots.id,
|
|
14484
|
+
snapshotCreatedAt: querySnapshots.createdAt,
|
|
14485
|
+
queryId: querySnapshots.queryId,
|
|
14486
|
+
queryText: querySnapshots.queryText,
|
|
14487
|
+
currentQuery: queries.query,
|
|
14488
|
+
provider: querySnapshots.provider,
|
|
14489
|
+
model: querySnapshots.model,
|
|
14490
|
+
location: querySnapshots.location,
|
|
14491
|
+
citationState: querySnapshots.citationState,
|
|
14492
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
14493
|
+
citedDomains: querySnapshots.citedDomains,
|
|
14494
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
14495
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
14496
|
+
answerText: querySnapshots.answerText,
|
|
14497
|
+
rawResponse: querySnapshots.rawResponse
|
|
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();
|
|
14499
|
+
const records = rows.map((row) => {
|
|
14500
|
+
const evidence = readEvidence(row.rawResponse);
|
|
14501
|
+
const citationState = row.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"];
|
|
14502
|
+
return {
|
|
14503
|
+
runId: row.runId,
|
|
14504
|
+
runKind: RunKinds["answer-visibility"],
|
|
14505
|
+
runStatus: runStatusSchema.parse(row.runStatus),
|
|
14506
|
+
runTrigger: runTriggerSchema.parse(row.runTrigger),
|
|
14507
|
+
runCreatedAt: row.runCreatedAt,
|
|
14508
|
+
runStartedAt: row.runStartedAt,
|
|
14509
|
+
runFinishedAt: row.runFinishedAt,
|
|
14510
|
+
snapshotId: row.snapshotId,
|
|
14511
|
+
snapshotCreatedAt: row.snapshotCreatedAt,
|
|
14512
|
+
queryId: row.queryId,
|
|
14513
|
+
query: row.queryText ?? row.currentQuery,
|
|
14514
|
+
provider: row.provider,
|
|
14515
|
+
model: row.model,
|
|
14516
|
+
location: row.location,
|
|
14517
|
+
citationState,
|
|
14518
|
+
cited: citationState === CitationStates.cited,
|
|
14519
|
+
answerMentioned: row.answerMentioned,
|
|
14520
|
+
mentionState: recordMentionState(row.answerMentioned),
|
|
14521
|
+
citedDomains: row.citedDomains,
|
|
14522
|
+
competitorOverlap: row.competitorOverlap,
|
|
14523
|
+
recommendedCompetitors: row.recommendedCompetitors,
|
|
14524
|
+
answerText: row.answerText,
|
|
14525
|
+
groundingSources: evidence.groundingSources,
|
|
14526
|
+
searchQueries: evidence.searchQueries
|
|
14527
|
+
};
|
|
14528
|
+
});
|
|
14529
|
+
const dto = {
|
|
14530
|
+
schemaVersion: "canonry.results-export/v1",
|
|
14531
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14532
|
+
project: {
|
|
14533
|
+
id: project.id,
|
|
14534
|
+
name: project.name,
|
|
14535
|
+
displayName: project.displayName,
|
|
14536
|
+
canonicalDomain: project.canonicalDomain,
|
|
14537
|
+
country: project.country,
|
|
14538
|
+
language: project.language
|
|
14539
|
+
},
|
|
14540
|
+
filters: {
|
|
14541
|
+
since: request.query.since || null,
|
|
14542
|
+
until: request.query.until || null,
|
|
14543
|
+
includeProbes
|
|
14544
|
+
},
|
|
14545
|
+
recordCount: records.length,
|
|
14546
|
+
records
|
|
14547
|
+
};
|
|
14548
|
+
reply.header("Content-Disposition", `attachment; filename="${filenameFor(project.name, format)}"`);
|
|
14549
|
+
reply.header("Cache-Control", "private, no-store");
|
|
14550
|
+
reply.header("X-Content-Type-Options", "nosniff");
|
|
14551
|
+
if (format === "csv") {
|
|
14552
|
+
reply.type("text/csv; charset=utf-8");
|
|
14553
|
+
return reply.send(renderResultsExportCsv(dto));
|
|
14554
|
+
}
|
|
14555
|
+
reply.type("application/json; charset=utf-8");
|
|
14556
|
+
return reply.send(dto);
|
|
14557
|
+
});
|
|
14558
|
+
}
|
|
14559
|
+
|
|
14242
14560
|
// ../api-routes/src/composites.ts
|
|
14243
|
-
import { eq as
|
|
14561
|
+
import { eq as eq20, and as and16, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
|
|
14244
14562
|
var TOP_INSIGHT_LIMIT = 5;
|
|
14245
14563
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
14246
14564
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -14258,7 +14576,7 @@ async function compositeRoutes(app) {
|
|
|
14258
14576
|
const project = resolveProject(app.db, request.params.name);
|
|
14259
14577
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
14260
14578
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
14261
|
-
const allRunsRaw = app.db.select().from(runs).where(
|
|
14579
|
+
const allRunsRaw = app.db.select().from(runs).where(and16(eq20(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
14262
14580
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
14263
14581
|
const totalRuns = allRuns.length;
|
|
14264
14582
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -14271,15 +14589,15 @@ async function compositeRoutes(app) {
|
|
|
14271
14589
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
14272
14590
|
const latestRunRow = allRuns[0] ?? null;
|
|
14273
14591
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
14274
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
14592
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq20(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
14275
14593
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
14276
|
-
const insightRows = app.db.select().from(insights).where(
|
|
14594
|
+
const insightRows = app.db.select().from(insights).where(eq20(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
14277
14595
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
14278
14596
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
14279
14597
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
14280
14598
|
for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
|
|
14281
14599
|
for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
|
|
14282
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
14600
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
|
|
14283
14601
|
const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
|
|
14284
14602
|
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
|
|
14285
14603
|
const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
@@ -14295,7 +14613,7 @@ async function compositeRoutes(app) {
|
|
|
14295
14613
|
trackedPrevious,
|
|
14296
14614
|
previousVisibilityRun?.createdAt ?? null
|
|
14297
14615
|
);
|
|
14298
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
14616
|
+
const competitorRows = app.db.select().from(competitors).where(eq20(competitors.projectId, project.id)).all();
|
|
14299
14617
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
14300
14618
|
for (const snapshots of snapshotsByRun.values()) {
|
|
14301
14619
|
for (const snapshot of snapshots) {
|
|
@@ -14413,9 +14731,9 @@ async function compositeRoutes(app) {
|
|
|
14413
14731
|
citedDomains: querySnapshots.citedDomains,
|
|
14414
14732
|
rawResponse: querySnapshots.rawResponse,
|
|
14415
14733
|
createdAt: querySnapshots.createdAt
|
|
14416
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
14417
|
-
|
|
14418
|
-
|
|
14734
|
+
}).from(querySnapshots).innerJoin(queries, eq20(querySnapshots.queryId, queries.id)).where(
|
|
14735
|
+
and16(
|
|
14736
|
+
eq20(queries.projectId, project.id),
|
|
14419
14737
|
or4(
|
|
14420
14738
|
sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
14421
14739
|
sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -14425,8 +14743,8 @@ async function compositeRoutes(app) {
|
|
|
14425
14743
|
)
|
|
14426
14744
|
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
14427
14745
|
const insightMatches = app.db.select().from(insights).where(
|
|
14428
|
-
|
|
14429
|
-
|
|
14746
|
+
and16(
|
|
14747
|
+
eq20(insights.projectId, project.id),
|
|
14430
14748
|
or4(
|
|
14431
14749
|
like2(insights.title, pattern),
|
|
14432
14750
|
like2(insights.query, pattern),
|
|
@@ -14642,8 +14960,8 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
14642
14960
|
// NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
|
|
14643
14961
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
14644
14962
|
avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
14645
|
-
}).from(gscSearchData).where(
|
|
14646
|
-
|
|
14963
|
+
}).from(gscSearchData).where(and16(
|
|
14964
|
+
eq20(gscSearchData.projectId, projectId),
|
|
14647
14965
|
sql8`${gscSearchData.date} >= ${cutoff}`,
|
|
14648
14966
|
sql8`${gscSearchData.impressions} > 0`
|
|
14649
14967
|
)).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
@@ -14666,8 +14984,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
14666
14984
|
tooltip,
|
|
14667
14985
|
trend: []
|
|
14668
14986
|
};
|
|
14669
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
14670
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
14987
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq20(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
14988
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq20(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
14671
14989
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
14672
14990
|
if (!chosen) return empty;
|
|
14673
14991
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -14693,7 +15011,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
14693
15011
|
url: gscUrlInspections.url,
|
|
14694
15012
|
indexingState: gscUrlInspections.indexingState,
|
|
14695
15013
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
14696
|
-
}).from(gscUrlInspections).where(
|
|
15014
|
+
}).from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
14697
15015
|
if (rows.length === 0) return 0;
|
|
14698
15016
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
14699
15017
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -14945,6 +15263,7 @@ var SCHEMA_TABLE = {
|
|
|
14945
15263
|
AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
|
|
14946
15264
|
AdsDisconnectResponse: adsDisconnectResponseSchema,
|
|
14947
15265
|
AdsInsightsResponse: adsInsightsResponseSchema,
|
|
15266
|
+
AdsOperationResponse: adsOperationResponseSchema,
|
|
14948
15267
|
AdsSummaryDto: adsSummaryDtoSchema,
|
|
14949
15268
|
AdsSyncResponse: adsSyncResponseSchema,
|
|
14950
15269
|
ApiKeyDto: apiKeyDtoSchema,
|
|
@@ -15013,6 +15332,7 @@ var SCHEMA_TABLE = {
|
|
|
15013
15332
|
GscSiteListResponseDto: gscSiteListResponseDtoSchema,
|
|
15014
15333
|
GscSitemapListResponseDto: gscSitemapListResponseDtoSchema,
|
|
15015
15334
|
GscUrlInspectionDto: gscUrlInspectionDtoSchema,
|
|
15335
|
+
HealthSnapshotDto: healthSnapshotDtoSchema,
|
|
15016
15336
|
IndexingRequestResponseDto: indexingRequestResponseDtoSchema,
|
|
15017
15337
|
KeywordDto: keywordDtoSchema,
|
|
15018
15338
|
LatestProjectRunDto: latestProjectRunDtoSchema,
|
|
@@ -15022,6 +15342,7 @@ var SCHEMA_TABLE = {
|
|
|
15022
15342
|
ProjectOverviewDto: projectOverviewDtoSchema,
|
|
15023
15343
|
ProjectReportDto: projectReportDtoSchema,
|
|
15024
15344
|
QueryDto: queryDtoSchema,
|
|
15345
|
+
ResultsExportDto: resultsExportDtoSchema,
|
|
15025
15346
|
RunDetailDto: runDetailDtoSchema,
|
|
15026
15347
|
RunDto: runDtoSchema,
|
|
15027
15348
|
SchedulableRunKind: schedulableRunKindSchema,
|
|
@@ -15125,6 +15446,17 @@ var booleanSchema = { type: "boolean" };
|
|
|
15125
15446
|
var integerSchema = { type: "integer" };
|
|
15126
15447
|
var objectSchema = { type: "object", additionalProperties: true };
|
|
15127
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
|
+
};
|
|
15128
15460
|
var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
|
|
15129
15461
|
var locationSchema = {
|
|
15130
15462
|
type: "object",
|
|
@@ -15526,6 +15858,62 @@ var routeCatalog = [
|
|
|
15526
15858
|
404: errorResponse("Project not found.")
|
|
15527
15859
|
}
|
|
15528
15860
|
},
|
|
15861
|
+
{
|
|
15862
|
+
method: "get",
|
|
15863
|
+
path: "/api/v1/projects/{name}/results/export",
|
|
15864
|
+
summary: "Download historical answer-engine results",
|
|
15865
|
+
tags: ["exports"],
|
|
15866
|
+
description: "Downloads one record per persisted answer-visibility query \xD7 provider observation. Citation and mention stay independent: `citationState` describes source-list attribution, while nullable `answerMentioned` describes answer-text presence. Excludes probe runs by default. JSON is a versioned portable artifact; CSV is its flat spreadsheet representation. Raw provider payloads, credentials, and local paths are never included.",
|
|
15867
|
+
parameters: [
|
|
15868
|
+
nameParameter,
|
|
15869
|
+
{
|
|
15870
|
+
name: "format",
|
|
15871
|
+
in: "query",
|
|
15872
|
+
description: "Download format. Defaults to `json`.",
|
|
15873
|
+
schema: { type: "string", enum: ["json", "csv"], default: "json" }
|
|
15874
|
+
},
|
|
15875
|
+
{
|
|
15876
|
+
name: "since",
|
|
15877
|
+
in: "query",
|
|
15878
|
+
description: "Inclusive ISO 8601 lower bound on run creation time.",
|
|
15879
|
+
schema: stringSchema
|
|
15880
|
+
},
|
|
15881
|
+
{
|
|
15882
|
+
name: "until",
|
|
15883
|
+
in: "query",
|
|
15884
|
+
description: "Inclusive ISO 8601 upper bound on run creation time. A date-only value includes that whole UTC day.",
|
|
15885
|
+
schema: stringSchema
|
|
15886
|
+
},
|
|
15887
|
+
{
|
|
15888
|
+
name: "includeProbes",
|
|
15889
|
+
in: "query",
|
|
15890
|
+
description: "Include operator/agent probe runs. Defaults to `false`.",
|
|
15891
|
+
schema: booleanSchema
|
|
15892
|
+
}
|
|
15893
|
+
],
|
|
15894
|
+
responses: {
|
|
15895
|
+
200: {
|
|
15896
|
+
description: "Results attachment returned.",
|
|
15897
|
+
content: {
|
|
15898
|
+
// The query-selected attachment can be JSON or CSV. OpenAPI cannot
|
|
15899
|
+
// express that the `format` parameter selects a media type, so keep
|
|
15900
|
+
// the generated SDK honest with a union rather than promising JSON
|
|
15901
|
+
// to a caller that requested CSV.
|
|
15902
|
+
"application/json": {
|
|
15903
|
+
schema: {
|
|
15904
|
+
oneOf: [
|
|
15905
|
+
{ $ref: "#/components/schemas/ResultsExportDto" },
|
|
15906
|
+
{ type: "string" }
|
|
15907
|
+
]
|
|
15908
|
+
}
|
|
15909
|
+
},
|
|
15910
|
+
"text/csv": { schema: { type: "string" } }
|
|
15911
|
+
}
|
|
15912
|
+
},
|
|
15913
|
+
400: errorResponse("Invalid export filters."),
|
|
15914
|
+
404: errorResponse("Project not found.")
|
|
15915
|
+
}
|
|
15916
|
+
},
|
|
15529
15917
|
{
|
|
15530
15918
|
method: "get",
|
|
15531
15919
|
path: "/api/v1/projects/{name}/queries",
|
|
@@ -16013,7 +16401,15 @@ var routeCatalog = [
|
|
|
16013
16401
|
path: "/api/v1/projects/{name}/history",
|
|
16014
16402
|
summary: "Get project audit history",
|
|
16015
16403
|
tags: ["history"],
|
|
16016
|
-
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
|
+
],
|
|
16017
16413
|
responses: {
|
|
16018
16414
|
200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
|
|
16019
16415
|
}
|
|
@@ -16023,6 +16419,14 @@ var routeCatalog = [
|
|
|
16023
16419
|
path: "/api/v1/history",
|
|
16024
16420
|
summary: "Get global audit history",
|
|
16025
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
|
+
],
|
|
16026
16430
|
responses: {
|
|
16027
16431
|
200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
|
|
16028
16432
|
}
|
|
@@ -16047,7 +16451,11 @@ var routeCatalog = [
|
|
|
16047
16451
|
path: "/api/v1/projects/{name}/timeline",
|
|
16048
16452
|
summary: "Get query timeline",
|
|
16049
16453
|
tags: ["history"],
|
|
16050
|
-
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
|
+
],
|
|
16051
16459
|
responses: {
|
|
16052
16460
|
// TODO: Add `ProjectTimelineDto` Zod schema in contracts.
|
|
16053
16461
|
200: rawJsonResponse("Timeline returned.", looseObjectSchema)
|
|
@@ -17168,6 +17576,7 @@ var routeCatalog = [
|
|
|
17168
17576
|
responses: {
|
|
17169
17577
|
200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
|
|
17170
17578
|
400: errorResponse("Missing/invalid key or credential storage unavailable."),
|
|
17579
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17171
17580
|
404: errorResponse("Project not found.")
|
|
17172
17581
|
}
|
|
17173
17582
|
},
|
|
@@ -17179,6 +17588,7 @@ var routeCatalog = [
|
|
|
17179
17588
|
parameters: [nameParameter],
|
|
17180
17589
|
responses: {
|
|
17181
17590
|
200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
|
|
17591
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17182
17592
|
404: errorResponse("Project not found.")
|
|
17183
17593
|
}
|
|
17184
17594
|
},
|
|
@@ -17193,6 +17603,288 @@ var routeCatalog = [
|
|
|
17193
17603
|
404: errorResponse("Project not found.")
|
|
17194
17604
|
}
|
|
17195
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
|
+
},
|
|
17196
17888
|
{
|
|
17197
17889
|
method: "post",
|
|
17198
17890
|
path: "/api/v1/projects/{name}/ads/sync",
|
|
@@ -17202,13 +17894,14 @@ var routeCatalog = [
|
|
|
17202
17894
|
responses: {
|
|
17203
17895
|
200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
|
|
17204
17896
|
400: errorResponse("No ads connection for this project."),
|
|
17897
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17205
17898
|
404: errorResponse("Project not found.")
|
|
17206
17899
|
}
|
|
17207
17900
|
},
|
|
17208
17901
|
{
|
|
17209
17902
|
method: "get",
|
|
17210
17903
|
path: "/api/v1/projects/{name}/ads/campaigns",
|
|
17211
|
-
summary: "Synced campaign snapshots with nested ad groups
|
|
17904
|
+
summary: "Synced campaign snapshots with lifecycle timestamps, targeting, nested ad groups, and ads",
|
|
17212
17905
|
tags: ["ads"],
|
|
17213
17906
|
parameters: [nameParameter],
|
|
17214
17907
|
responses: {
|
|
@@ -18118,8 +18811,7 @@ var routeCatalog = [
|
|
|
18118
18811
|
tags: ["intelligence"],
|
|
18119
18812
|
parameters: [nameParameter],
|
|
18120
18813
|
responses: {
|
|
18121
|
-
|
|
18122
|
-
200: rawJsonResponse("Health snapshot or no-data sentinel returned.", looseObjectSchema),
|
|
18814
|
+
200: jsonResponse("Health snapshot or no-data sentinel returned.", "HealthSnapshotDto"),
|
|
18123
18815
|
404: errorResponse("Project not found.")
|
|
18124
18816
|
}
|
|
18125
18817
|
},
|
|
@@ -18130,11 +18822,10 @@ var routeCatalog = [
|
|
|
18130
18822
|
tags: ["intelligence"],
|
|
18131
18823
|
parameters: [
|
|
18132
18824
|
nameParameter,
|
|
18133
|
-
{ name: "limit", in: "query", description: "Max results.", schema:
|
|
18825
|
+
{ name: "limit", in: "query", description: "Max results.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
|
|
18134
18826
|
],
|
|
18135
18827
|
responses: {
|
|
18136
|
-
|
|
18137
|
-
200: rawJsonResponse("Health history returned.", { type: "array", items: looseObjectSchema }),
|
|
18828
|
+
200: jsonArrayResponse("Health history returned.", "HealthSnapshotDto"),
|
|
18138
18829
|
404: errorResponse("Project not found.")
|
|
18139
18830
|
}
|
|
18140
18831
|
},
|
|
@@ -19035,22 +19726,26 @@ var routeCatalog = [
|
|
|
19035
19726
|
method: "get",
|
|
19036
19727
|
path: "/api/v1/projects/{name}/technical-aeo",
|
|
19037
19728
|
summary: "Get the Technical AEO scorecard for a project",
|
|
19038
|
-
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.",
|
|
19039
19730
|
tags: ["technical-aeo"],
|
|
19040
|
-
parameters: [
|
|
19731
|
+
parameters: [
|
|
19732
|
+
nameParameter,
|
|
19733
|
+
{ name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema }
|
|
19734
|
+
],
|
|
19041
19735
|
responses: {
|
|
19042
19736
|
200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
|
|
19043
|
-
404: errorResponse("Project not found.")
|
|
19737
|
+
404: errorResponse("Project or site-audit run not found.")
|
|
19044
19738
|
}
|
|
19045
19739
|
},
|
|
19046
19740
|
{
|
|
19047
19741
|
method: "get",
|
|
19048
19742
|
path: "/api/v1/projects/{name}/technical-aeo/pages",
|
|
19049
|
-
summary: "List audited pages from
|
|
19050
|
-
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.",
|
|
19051
19745
|
tags: ["technical-aeo"],
|
|
19052
19746
|
parameters: [
|
|
19053
19747
|
nameParameter,
|
|
19748
|
+
{ name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema },
|
|
19054
19749
|
{ name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
|
|
19055
19750
|
{ name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
|
|
19056
19751
|
limitQueryParameter,
|
|
@@ -19058,7 +19753,7 @@ var routeCatalog = [
|
|
|
19058
19753
|
],
|
|
19059
19754
|
responses: {
|
|
19060
19755
|
200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
|
|
19061
|
-
404: errorResponse("Project not found.")
|
|
19756
|
+
404: errorResponse("Project or site-audit run not found.")
|
|
19062
19757
|
}
|
|
19063
19758
|
},
|
|
19064
19759
|
{
|
|
@@ -19432,7 +20127,7 @@ async function settingsRoutes(app, opts) {
|
|
|
19432
20127
|
|
|
19433
20128
|
// ../api-routes/src/keys.ts
|
|
19434
20129
|
import crypto13 from "crypto";
|
|
19435
|
-
import { desc as desc10, eq as
|
|
20130
|
+
import { desc as desc10, eq as eq21 } from "drizzle-orm";
|
|
19436
20131
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
19437
20132
|
function toApiKeyDto(row) {
|
|
19438
20133
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -19451,7 +20146,7 @@ function toApiKeyDto(row) {
|
|
|
19451
20146
|
async function keysRoutes(app) {
|
|
19452
20147
|
app.get("/keys", async (request) => {
|
|
19453
20148
|
const scopedProjectId = request.apiKey?.projectId;
|
|
19454
|
-
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(
|
|
20149
|
+
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq21(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
19455
20150
|
return { keys: rows.map(toApiKeyDto) };
|
|
19456
20151
|
});
|
|
19457
20152
|
app.get("/keys/self", async (request) => {
|
|
@@ -19459,7 +20154,7 @@ async function keysRoutes(app) {
|
|
|
19459
20154
|
if (!id) {
|
|
19460
20155
|
throw notFound("API key", "self");
|
|
19461
20156
|
}
|
|
19462
|
-
const row = app.db.select().from(apiKeys).where(
|
|
20157
|
+
const row = app.db.select().from(apiKeys).where(eq21(apiKeys.id, id)).get();
|
|
19463
20158
|
if (!row) {
|
|
19464
20159
|
throw notFound("API key", id);
|
|
19465
20160
|
}
|
|
@@ -19477,7 +20172,7 @@ async function keysRoutes(app) {
|
|
|
19477
20172
|
throw forbidden("A project-scoped key can only mint keys scoped to its own project.");
|
|
19478
20173
|
}
|
|
19479
20174
|
if (projectId) {
|
|
19480
|
-
const proj = app.db.select({ id: projects.id }).from(projects).where(
|
|
20175
|
+
const proj = app.db.select({ id: projects.id }).from(projects).where(eq21(projects.id, projectId)).get();
|
|
19481
20176
|
if (!proj) {
|
|
19482
20177
|
throw notFound("Project", projectId);
|
|
19483
20178
|
}
|
|
@@ -19523,7 +20218,7 @@ async function keysRoutes(app) {
|
|
|
19523
20218
|
app.post("/keys/:id/revoke", async (request) => {
|
|
19524
20219
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
19525
20220
|
const { id } = request.params;
|
|
19526
|
-
const row = app.db.select().from(apiKeys).where(
|
|
20221
|
+
const row = app.db.select().from(apiKeys).where(eq21(apiKeys.id, id)).get();
|
|
19527
20222
|
if (!row) {
|
|
19528
20223
|
throw notFound("API key", id);
|
|
19529
20224
|
}
|
|
@@ -19539,7 +20234,7 @@ async function keysRoutes(app) {
|
|
|
19539
20234
|
}
|
|
19540
20235
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19541
20236
|
app.db.transaction((tx) => {
|
|
19542
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
20237
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq21(apiKeys.id, id)).run();
|
|
19543
20238
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
19544
20239
|
actor: "api",
|
|
19545
20240
|
action: "api-key.revoked",
|
|
@@ -19612,7 +20307,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
19612
20307
|
|
|
19613
20308
|
// ../api-routes/src/schedules.ts
|
|
19614
20309
|
import crypto14 from "crypto";
|
|
19615
|
-
import { and as
|
|
20310
|
+
import { and as and17, eq as eq22 } from "drizzle-orm";
|
|
19616
20311
|
function parseKindParam(raw) {
|
|
19617
20312
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
19618
20313
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -19639,7 +20334,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19639
20334
|
if (!sourceId) {
|
|
19640
20335
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
19641
20336
|
}
|
|
19642
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
20337
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq22(trafficSources.id, sourceId)).get();
|
|
19643
20338
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
19644
20339
|
throw notFound("Traffic source", sourceId);
|
|
19645
20340
|
}
|
|
@@ -19684,7 +20379,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19684
20379
|
}
|
|
19685
20380
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19686
20381
|
const enabledBool = enabled !== false;
|
|
19687
|
-
const existing = app.db.select().from(schedules).where(
|
|
20382
|
+
const existing = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
|
|
19688
20383
|
if (existing) {
|
|
19689
20384
|
app.db.update(schedules).set({
|
|
19690
20385
|
cronExpr,
|
|
@@ -19694,7 +20389,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19694
20389
|
sourceId: sourceId ?? null,
|
|
19695
20390
|
enabled: enabledBool,
|
|
19696
20391
|
updatedAt: now
|
|
19697
|
-
}).where(
|
|
20392
|
+
}).where(eq22(schedules.id, existing.id)).run();
|
|
19698
20393
|
} else {
|
|
19699
20394
|
app.db.insert(schedules).values({
|
|
19700
20395
|
id: crypto14.randomUUID(),
|
|
@@ -19718,13 +20413,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
19718
20413
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
19719
20414
|
});
|
|
19720
20415
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
19721
|
-
const schedule = app.db.select().from(schedules).where(
|
|
20416
|
+
const schedule = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
|
|
19722
20417
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
19723
20418
|
});
|
|
19724
20419
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
19725
20420
|
const project = resolveProject(app.db, request.params.name);
|
|
19726
20421
|
const kind = parseKindParam(request.query?.kind);
|
|
19727
|
-
const schedule = app.db.select().from(schedules).where(
|
|
20422
|
+
const schedule = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
|
|
19728
20423
|
if (!schedule) {
|
|
19729
20424
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
19730
20425
|
}
|
|
@@ -19733,11 +20428,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
19733
20428
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
19734
20429
|
const project = resolveProject(app.db, request.params.name);
|
|
19735
20430
|
const kind = parseKindParam(request.query?.kind);
|
|
19736
|
-
const schedule = app.db.select().from(schedules).where(
|
|
20431
|
+
const schedule = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
|
|
19737
20432
|
if (!schedule) {
|
|
19738
20433
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
19739
20434
|
}
|
|
19740
|
-
app.db.delete(schedules).where(
|
|
20435
|
+
app.db.delete(schedules).where(eq22(schedules.id, schedule.id)).run();
|
|
19741
20436
|
writeAuditLog(app.db, {
|
|
19742
20437
|
projectId: project.id,
|
|
19743
20438
|
actor: "api",
|
|
@@ -19770,7 +20465,7 @@ function formatSchedule(row) {
|
|
|
19770
20465
|
|
|
19771
20466
|
// ../api-routes/src/notifications.ts
|
|
19772
20467
|
import crypto15 from "crypto";
|
|
19773
|
-
import { eq as
|
|
20468
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
19774
20469
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
19775
20470
|
async function notificationRoutes(app, opts = {}) {
|
|
19776
20471
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -19810,22 +20505,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19810
20505
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
19811
20506
|
});
|
|
19812
20507
|
return reply.status(201).send({
|
|
19813
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
20508
|
+
...formatNotification(app.db.select().from(notifications).where(eq23(notifications.id, id)).get()),
|
|
19814
20509
|
webhookSecret
|
|
19815
20510
|
});
|
|
19816
20511
|
});
|
|
19817
20512
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
19818
20513
|
const project = resolveProject(app.db, request.params.name);
|
|
19819
|
-
const rows = app.db.select().from(notifications).where(
|
|
20514
|
+
const rows = app.db.select().from(notifications).where(eq23(notifications.projectId, project.id)).all();
|
|
19820
20515
|
return reply.send(rows.map(formatNotification));
|
|
19821
20516
|
});
|
|
19822
20517
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
19823
20518
|
const project = resolveProject(app.db, request.params.name);
|
|
19824
|
-
const notification = app.db.select().from(notifications).where(
|
|
20519
|
+
const notification = app.db.select().from(notifications).where(eq23(notifications.id, request.params.id)).get();
|
|
19825
20520
|
if (!notification || notification.projectId !== project.id) {
|
|
19826
20521
|
throw notFound("Notification", request.params.id);
|
|
19827
20522
|
}
|
|
19828
|
-
app.db.delete(notifications).where(
|
|
20523
|
+
app.db.delete(notifications).where(eq23(notifications.id, notification.id)).run();
|
|
19829
20524
|
writeAuditLog(app.db, {
|
|
19830
20525
|
projectId: project.id,
|
|
19831
20526
|
actor: "api",
|
|
@@ -19837,7 +20532,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19837
20532
|
});
|
|
19838
20533
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
19839
20534
|
const project = resolveProject(app.db, request.params.name);
|
|
19840
|
-
const notification = app.db.select().from(notifications).where(
|
|
20535
|
+
const notification = app.db.select().from(notifications).where(eq23(notifications.id, request.params.id)).get();
|
|
19841
20536
|
if (!notification || notification.projectId !== project.id) {
|
|
19842
20537
|
throw notFound("Notification", request.params.id);
|
|
19843
20538
|
}
|
|
@@ -19890,7 +20585,7 @@ function formatNotification(row) {
|
|
|
19890
20585
|
|
|
19891
20586
|
// ../api-routes/src/google.ts
|
|
19892
20587
|
import crypto19 from "crypto";
|
|
19893
|
-
import { eq as
|
|
20588
|
+
import { eq as eq24, and as and18, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
|
|
19894
20589
|
|
|
19895
20590
|
// ../api-routes/src/gbp-summary.ts
|
|
19896
20591
|
function computeMetricTotals(rows) {
|
|
@@ -21730,7 +22425,7 @@ async function googleRoutes(app, opts) {
|
|
|
21730
22425
|
if (!projectId) {
|
|
21731
22426
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
21732
22427
|
}
|
|
21733
|
-
const project = app.db.select().from(projects).where(
|
|
22428
|
+
const project = app.db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
21734
22429
|
if (!project) {
|
|
21735
22430
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
21736
22431
|
}
|
|
@@ -21862,14 +22557,14 @@ async function googleRoutes(app, opts) {
|
|
|
21862
22557
|
if (opts.onGscSyncRequested) {
|
|
21863
22558
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
21864
22559
|
}
|
|
21865
|
-
const run = app.db.select().from(runs).where(
|
|
22560
|
+
const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
|
|
21866
22561
|
return run;
|
|
21867
22562
|
});
|
|
21868
22563
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
21869
22564
|
const project = resolveProject(app.db, request.params.name);
|
|
21870
22565
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
21871
22566
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
21872
|
-
const conditions = [
|
|
22567
|
+
const conditions = [eq24(gscSearchData.projectId, project.id)];
|
|
21873
22568
|
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
21874
22569
|
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
21875
22570
|
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -21877,7 +22572,7 @@ async function googleRoutes(app, opts) {
|
|
|
21877
22572
|
if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
21878
22573
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
21879
22574
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
21880
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
22575
|
+
const rows = app.db.select().from(gscSearchData).where(and18(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
21881
22576
|
return rows.map((r) => ({
|
|
21882
22577
|
date: r.date,
|
|
21883
22578
|
query: r.query,
|
|
@@ -21897,7 +22592,7 @@ async function googleRoutes(app, opts) {
|
|
|
21897
22592
|
const windowStart = startDate ?? cutoffDate ?? "";
|
|
21898
22593
|
const windowEnd = endDate ?? "9999-12-31";
|
|
21899
22594
|
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
21900
|
-
const conditions = [
|
|
22595
|
+
const conditions = [eq24(gscSearchData.projectId, project.id)];
|
|
21901
22596
|
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
21902
22597
|
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
21903
22598
|
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -21905,7 +22600,7 @@ async function googleRoutes(app, opts) {
|
|
|
21905
22600
|
date: gscSearchData.date,
|
|
21906
22601
|
clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
21907
22602
|
impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
21908
|
-
}).from(gscSearchData).where(
|
|
22603
|
+
}).from(gscSearchData).where(and18(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
21909
22604
|
const daily = mergeGscDailyTotalsWithFallback(
|
|
21910
22605
|
dailyTotals,
|
|
21911
22606
|
dimensionedRows.map((r) => ({
|
|
@@ -21996,9 +22691,9 @@ async function googleRoutes(app, opts) {
|
|
|
21996
22691
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
21997
22692
|
const project = resolveProject(app.db, request.params.name);
|
|
21998
22693
|
const { url, limit } = request.query;
|
|
21999
|
-
const conditions = [
|
|
22000
|
-
if (url) conditions.push(
|
|
22001
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
22694
|
+
const conditions = [eq24(gscUrlInspections.projectId, project.id)];
|
|
22695
|
+
if (url) conditions.push(eq24(gscUrlInspections.url, url));
|
|
22696
|
+
const rows = app.db.select().from(gscUrlInspections).where(and18(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
22002
22697
|
return rows.map((r) => ({
|
|
22003
22698
|
id: r.id,
|
|
22004
22699
|
url: r.url,
|
|
@@ -22017,7 +22712,7 @@ async function googleRoutes(app, opts) {
|
|
|
22017
22712
|
});
|
|
22018
22713
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
22019
22714
|
const project = resolveProject(app.db, request.params.name);
|
|
22020
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22715
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
22021
22716
|
const byUrl = /* @__PURE__ */ new Map();
|
|
22022
22717
|
for (const row of allInspections) {
|
|
22023
22718
|
const existing = byUrl.get(row.url);
|
|
@@ -22045,7 +22740,7 @@ async function googleRoutes(app, opts) {
|
|
|
22045
22740
|
});
|
|
22046
22741
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
22047
22742
|
const project = resolveProject(app.db, request.params.name);
|
|
22048
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22743
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
22049
22744
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
22050
22745
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22051
22746
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -22094,7 +22789,7 @@ async function googleRoutes(app, opts) {
|
|
|
22094
22789
|
const total = latestByUrl.size;
|
|
22095
22790
|
const indexed = indexedUrls.length;
|
|
22096
22791
|
const notIndexed = notIndexedUrls.length;
|
|
22097
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
22792
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq24(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
22098
22793
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
22099
22794
|
const formatRow = (r) => ({
|
|
22100
22795
|
id: r.id,
|
|
@@ -22145,7 +22840,7 @@ async function googleRoutes(app, opts) {
|
|
|
22145
22840
|
const project = resolveProject(app.db, request.params.name);
|
|
22146
22841
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
22147
22842
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
22148
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
22843
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq24(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
22149
22844
|
return rows.map((r) => ({
|
|
22150
22845
|
date: r.date,
|
|
22151
22846
|
indexed: r.indexed,
|
|
@@ -22214,7 +22909,7 @@ async function googleRoutes(app, opts) {
|
|
|
22214
22909
|
if (opts.onInspectSitemapRequested) {
|
|
22215
22910
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
22216
22911
|
}
|
|
22217
|
-
const run = app.db.select().from(runs).where(
|
|
22912
|
+
const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
|
|
22218
22913
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
22219
22914
|
});
|
|
22220
22915
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -22241,7 +22936,7 @@ async function googleRoutes(app, opts) {
|
|
|
22241
22936
|
if (opts.onInspectSitemapRequested) {
|
|
22242
22937
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
22243
22938
|
}
|
|
22244
|
-
const run = app.db.select().from(runs).where(
|
|
22939
|
+
const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
|
|
22245
22940
|
return run;
|
|
22246
22941
|
});
|
|
22247
22942
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -22288,7 +22983,7 @@ async function googleRoutes(app, opts) {
|
|
|
22288
22983
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
22289
22984
|
let urlsToNotify = request.body?.urls ?? [];
|
|
22290
22985
|
if (request.body?.allUnindexed) {
|
|
22291
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22986
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
22292
22987
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22293
22988
|
for (const row of allInspections) {
|
|
22294
22989
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -22406,7 +23101,7 @@ async function googleRoutes(app, opts) {
|
|
|
22406
23101
|
};
|
|
22407
23102
|
}
|
|
22408
23103
|
function listSelectionResponse(projectId) {
|
|
22409
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
23104
|
+
const rows = app.db.select().from(gbpLocations).where(eq24(gbpLocations.projectId, projectId)).all();
|
|
22410
23105
|
const dtos = rows.map(rowToDto2);
|
|
22411
23106
|
return {
|
|
22412
23107
|
locations: dtos,
|
|
@@ -22415,15 +23110,15 @@ async function googleRoutes(app, opts) {
|
|
|
22415
23110
|
};
|
|
22416
23111
|
}
|
|
22417
23112
|
function clearGbpProjectData(tx, projectId) {
|
|
22418
|
-
tx.delete(gbpDailyMetrics).where(
|
|
22419
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
22420
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
22421
|
-
tx.delete(gbpPlaceActions).where(
|
|
22422
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
22423
|
-
tx.delete(gbpLocations).where(
|
|
23113
|
+
tx.delete(gbpDailyMetrics).where(eq24(gbpDailyMetrics.projectId, projectId)).run();
|
|
23114
|
+
tx.delete(gbpKeywordImpressions).where(eq24(gbpKeywordImpressions.projectId, projectId)).run();
|
|
23115
|
+
tx.delete(gbpKeywordMonthly).where(eq24(gbpKeywordMonthly.projectId, projectId)).run();
|
|
23116
|
+
tx.delete(gbpPlaceActions).where(eq24(gbpPlaceActions.projectId, projectId)).run();
|
|
23117
|
+
tx.delete(gbpLodgingSnapshots).where(eq24(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
23118
|
+
tx.delete(gbpLocations).where(eq24(gbpLocations.projectId, projectId)).run();
|
|
22424
23119
|
}
|
|
22425
23120
|
function currentProjectAccount(projectId) {
|
|
22426
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
23121
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq24(gbpLocations.projectId, projectId)).limit(1).get();
|
|
22427
23122
|
return row?.accountName ?? null;
|
|
22428
23123
|
}
|
|
22429
23124
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -22493,7 +23188,7 @@ async function googleRoutes(app, opts) {
|
|
|
22493
23188
|
app.db.transaction((tx) => {
|
|
22494
23189
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
22495
23190
|
for (const remote of remoteLocations) {
|
|
22496
|
-
const existing = tx.select().from(gbpLocations).where(
|
|
23191
|
+
const existing = tx.select().from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), eq24(gbpLocations.locationName, remote.name))).get();
|
|
22497
23192
|
const profile = buildLocationProfileFields(remote);
|
|
22498
23193
|
if (existing) {
|
|
22499
23194
|
tx.update(gbpLocations).set({
|
|
@@ -22506,7 +23201,7 @@ async function googleRoutes(app, opts) {
|
|
|
22506
23201
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
22507
23202
|
...profile,
|
|
22508
23203
|
updatedAt: now
|
|
22509
|
-
}).where(
|
|
23204
|
+
}).where(eq24(gbpLocations.id, existing.id)).run();
|
|
22510
23205
|
} else {
|
|
22511
23206
|
tx.insert(gbpLocations).values({
|
|
22512
23207
|
id: crypto19.randomUUID(),
|
|
@@ -22590,11 +23285,11 @@ async function googleRoutes(app, opts) {
|
|
|
22590
23285
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
22591
23286
|
}
|
|
22592
23287
|
const { selected } = parsed.data;
|
|
22593
|
-
const existing = app.db.select().from(gbpLocations).where(
|
|
23288
|
+
const existing = app.db.select().from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), eq24(gbpLocations.locationName, locationName))).get();
|
|
22594
23289
|
if (!existing) throw notFound("GBP location", locationName);
|
|
22595
23290
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22596
23291
|
app.db.transaction((tx) => {
|
|
22597
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
23292
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq24(gbpLocations.id, existing.id)).run();
|
|
22598
23293
|
writeAuditLog(tx, {
|
|
22599
23294
|
projectId: project.id,
|
|
22600
23295
|
actor: "api",
|
|
@@ -22603,7 +23298,7 @@ async function googleRoutes(app, opts) {
|
|
|
22603
23298
|
entityId: locationName
|
|
22604
23299
|
});
|
|
22605
23300
|
});
|
|
22606
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
23301
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq24(gbpLocations.id, existing.id)).get();
|
|
22607
23302
|
return rowToDto2(refreshed);
|
|
22608
23303
|
});
|
|
22609
23304
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -22647,10 +23342,10 @@ async function googleRoutes(app, opts) {
|
|
|
22647
23342
|
});
|
|
22648
23343
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
22649
23344
|
const project = resolveProject(app.db, request.params.name);
|
|
22650
|
-
const conditions = [
|
|
22651
|
-
if (request.query.locationName) conditions.push(
|
|
22652
|
-
if (request.query.metric) conditions.push(
|
|
22653
|
-
const rows = app.db.select().from(gbpDailyMetrics).where(
|
|
23345
|
+
const conditions = [eq24(gbpDailyMetrics.projectId, project.id)];
|
|
23346
|
+
if (request.query.locationName) conditions.push(eq24(gbpDailyMetrics.locationName, request.query.locationName));
|
|
23347
|
+
if (request.query.metric) conditions.push(eq24(gbpDailyMetrics.metric, request.query.metric));
|
|
23348
|
+
const rows = app.db.select().from(gbpDailyMetrics).where(and18(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
22654
23349
|
return {
|
|
22655
23350
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
22656
23351
|
total: rows.length
|
|
@@ -22658,9 +23353,9 @@ async function googleRoutes(app, opts) {
|
|
|
22658
23353
|
});
|
|
22659
23354
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
22660
23355
|
const project = resolveProject(app.db, request.params.name);
|
|
22661
|
-
const conditions = [
|
|
22662
|
-
if (request.query.locationName) conditions.push(
|
|
22663
|
-
const rows = app.db.select().from(gbpKeywordImpressions).where(
|
|
23356
|
+
const conditions = [eq24(gbpKeywordImpressions.projectId, project.id)];
|
|
23357
|
+
if (request.query.locationName) conditions.push(eq24(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
23358
|
+
const rows = app.db.select().from(gbpKeywordImpressions).where(and18(...conditions)).all();
|
|
22664
23359
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
22665
23360
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
22666
23361
|
return {
|
|
@@ -22678,9 +23373,9 @@ async function googleRoutes(app, opts) {
|
|
|
22678
23373
|
});
|
|
22679
23374
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
22680
23375
|
const project = resolveProject(app.db, request.params.name);
|
|
22681
|
-
const conditions = [
|
|
22682
|
-
if (request.query.locationName) conditions.push(
|
|
22683
|
-
const rows = app.db.select().from(gbpPlaceActions).where(
|
|
23376
|
+
const conditions = [eq24(gbpPlaceActions.projectId, project.id)];
|
|
23377
|
+
if (request.query.locationName) conditions.push(eq24(gbpPlaceActions.locationName, request.query.locationName));
|
|
23378
|
+
const rows = app.db.select().from(gbpPlaceActions).where(and18(...conditions)).all();
|
|
22684
23379
|
return {
|
|
22685
23380
|
placeActions: rows.map((r) => ({
|
|
22686
23381
|
locationName: r.locationName,
|
|
@@ -22695,9 +23390,9 @@ async function googleRoutes(app, opts) {
|
|
|
22695
23390
|
});
|
|
22696
23391
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
22697
23392
|
const project = resolveProject(app.db, request.params.name);
|
|
22698
|
-
const conditions = [
|
|
22699
|
-
if (request.query.locationName) conditions.push(
|
|
22700
|
-
const rows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
23393
|
+
const conditions = [eq24(gbpLodgingSnapshots.projectId, project.id)];
|
|
23394
|
+
if (request.query.locationName) conditions.push(eq24(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
23395
|
+
const rows = app.db.select().from(gbpLodgingSnapshots).where(and18(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
22701
23396
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22702
23397
|
for (const row of rows) {
|
|
22703
23398
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -22712,9 +23407,9 @@ async function googleRoutes(app, opts) {
|
|
|
22712
23407
|
});
|
|
22713
23408
|
app.get("/projects/:name/gbp/attributes", async (request) => {
|
|
22714
23409
|
const project = resolveProject(app.db, request.params.name);
|
|
22715
|
-
const conditions = [
|
|
22716
|
-
if (request.query.locationName) conditions.push(
|
|
22717
|
-
const rows = app.db.select().from(gbpAttributesSnapshots).where(
|
|
23410
|
+
const conditions = [eq24(gbpAttributesSnapshots.projectId, project.id)];
|
|
23411
|
+
if (request.query.locationName) conditions.push(eq24(gbpAttributesSnapshots.locationName, request.query.locationName));
|
|
23412
|
+
const rows = app.db.select().from(gbpAttributesSnapshots).where(and18(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
|
|
22718
23413
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22719
23414
|
for (const row of rows) {
|
|
22720
23415
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -22732,9 +23427,9 @@ async function googleRoutes(app, opts) {
|
|
|
22732
23427
|
});
|
|
22733
23428
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
22734
23429
|
const project = resolveProject(app.db, request.params.name);
|
|
22735
|
-
const conditions = [
|
|
22736
|
-
if (request.query.locationName) conditions.push(
|
|
22737
|
-
const rows = app.db.select().from(gbpPlaceDetails).where(
|
|
23430
|
+
const conditions = [eq24(gbpPlaceDetails.projectId, project.id)];
|
|
23431
|
+
if (request.query.locationName) conditions.push(eq24(gbpPlaceDetails.locationName, request.query.locationName));
|
|
23432
|
+
const rows = app.db.select().from(gbpPlaceDetails).where(and18(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
22738
23433
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22739
23434
|
for (const row of rows) {
|
|
22740
23435
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -22753,7 +23448,7 @@ async function googleRoutes(app, opts) {
|
|
|
22753
23448
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
22754
23449
|
const project = resolveProject(app.db, request.params.name);
|
|
22755
23450
|
const locationName = request.query.locationName ?? null;
|
|
22756
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(
|
|
23451
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), eq24(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
22757
23452
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22758
23453
|
if (locationNames.length === 0) {
|
|
22759
23454
|
return buildGbpSummary({
|
|
@@ -22767,10 +23462,10 @@ async function googleRoutes(app, opts) {
|
|
|
22767
23462
|
locationProfiles: []
|
|
22768
23463
|
});
|
|
22769
23464
|
}
|
|
22770
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(
|
|
22771
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(
|
|
22772
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(
|
|
22773
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
23465
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and18(eq24(gbpDailyMetrics.projectId, project.id), inArray11(gbpDailyMetrics.locationName, locationNames))).all();
|
|
23466
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and18(eq24(gbpKeywordImpressions.projectId, project.id), inArray11(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
23467
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and18(eq24(gbpPlaceActions.projectId, project.id), inArray11(gbpPlaceActions.locationName, locationNames))).all();
|
|
23468
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and18(eq24(gbpLodgingSnapshots.projectId, project.id), inArray11(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
22774
23469
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
22775
23470
|
for (const row of lodgingRows) {
|
|
22776
23471
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -22784,7 +23479,7 @@ async function googleRoutes(app, opts) {
|
|
|
22784
23479
|
regularHours: gbpLocations.regularHours,
|
|
22785
23480
|
primaryPhone: gbpLocations.primaryPhone,
|
|
22786
23481
|
openStatus: gbpLocations.openStatus
|
|
22787
|
-
}).from(gbpLocations).where(
|
|
23482
|
+
}).from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), inArray11(gbpLocations.locationName, locationNames))).all();
|
|
22788
23483
|
return buildGbpSummary({
|
|
22789
23484
|
locationName,
|
|
22790
23485
|
locationCount: locationNames.length,
|
|
@@ -22800,7 +23495,17 @@ async function googleRoutes(app, opts) {
|
|
|
22800
23495
|
|
|
22801
23496
|
// ../api-routes/src/ads.ts
|
|
22802
23497
|
import crypto20 from "crypto";
|
|
22803
|
-
import { eq 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
|
+
};
|
|
22804
23509
|
function statusDto(row) {
|
|
22805
23510
|
if (!row) return { connected: false };
|
|
22806
23511
|
return {
|
|
@@ -22821,15 +23526,185 @@ function creativeDto(raw) {
|
|
|
22821
23526
|
type: typeof c.type === "string" ? c.type : null,
|
|
22822
23527
|
title: typeof c.title === "string" ? c.title : null,
|
|
22823
23528
|
body: typeof c.body === "string" ? c.body : null,
|
|
22824
|
-
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
|
|
23558
|
+
};
|
|
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
|
|
22825
23585
|
};
|
|
22826
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
|
+
}
|
|
22827
23701
|
async function adsRoutes(app, opts) {
|
|
22828
23702
|
app.post(
|
|
22829
23703
|
"/projects/:name/ads/connect",
|
|
22830
23704
|
async (request) => {
|
|
23705
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
22831
23706
|
const project = resolveProject(app.db, request.params.name);
|
|
22832
|
-
const parsed = adsConnectRequestSchema.safeParse(request.body
|
|
23707
|
+
const parsed = adsConnectRequestSchema.safeParse(request.body);
|
|
22833
23708
|
if (!parsed.success) throw validationError('"apiKey" is required');
|
|
22834
23709
|
if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
|
|
22835
23710
|
throw validationError("Ads credential storage is not configured for this deployment");
|
|
@@ -22850,7 +23725,7 @@ async function adsRoutes(app, opts) {
|
|
|
22850
23725
|
createdAt: existingCfg?.createdAt ?? now,
|
|
22851
23726
|
updatedAt: now
|
|
22852
23727
|
});
|
|
22853
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
23728
|
+
const existingRow = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
22854
23729
|
app.db.transaction((tx) => {
|
|
22855
23730
|
if (existingRow) {
|
|
22856
23731
|
tx.update(adsConnections).set({
|
|
@@ -22860,7 +23735,7 @@ async function adsRoutes(app, opts) {
|
|
|
22860
23735
|
timezone: account.timezone,
|
|
22861
23736
|
status: account.status,
|
|
22862
23737
|
updatedAt: now
|
|
22863
|
-
}).where(
|
|
23738
|
+
}).where(eq25(adsConnections.id, existingRow.id)).run();
|
|
22864
23739
|
} else {
|
|
22865
23740
|
tx.insert(adsConnections).values({
|
|
22866
23741
|
id: crypto20.randomUUID(),
|
|
@@ -22882,16 +23757,17 @@ async function adsRoutes(app, opts) {
|
|
|
22882
23757
|
entityId: account.id
|
|
22883
23758
|
}));
|
|
22884
23759
|
});
|
|
22885
|
-
const row = app.db.select().from(adsConnections).where(
|
|
23760
|
+
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
22886
23761
|
return statusDto(row);
|
|
22887
23762
|
}
|
|
22888
23763
|
);
|
|
22889
23764
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
23765
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
22890
23766
|
const project = resolveProject(app.db, request.params.name);
|
|
22891
|
-
const row = app.db.select().from(adsConnections).where(
|
|
23767
|
+
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
22892
23768
|
if (row) {
|
|
22893
23769
|
app.db.transaction((tx) => {
|
|
22894
|
-
tx.delete(adsConnections).where(
|
|
23770
|
+
tx.delete(adsConnections).where(eq25(adsConnections.id, row.id)).run();
|
|
22895
23771
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
22896
23772
|
projectId: project.id,
|
|
22897
23773
|
actor: "api",
|
|
@@ -22907,18 +23783,232 @@ async function adsRoutes(app, opts) {
|
|
|
22907
23783
|
});
|
|
22908
23784
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
22909
23785
|
const project = resolveProject(app.db, request.params.name);
|
|
22910
|
-
const row = app.db.select().from(adsConnections).where(
|
|
23786
|
+
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
22911
23787
|
return statusDto(row);
|
|
22912
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
|
+
);
|
|
22913
24002
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
24003
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
22914
24004
|
const project = resolveProject(app.db, request.params.name);
|
|
22915
|
-
const row = app.db.select().from(adsConnections).where(
|
|
24005
|
+
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
22916
24006
|
if (!row) {
|
|
22917
24007
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
22918
24008
|
}
|
|
22919
|
-
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
22920
|
-
|
|
22921
|
-
|
|
24009
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and19(
|
|
24010
|
+
eq25(runs.projectId, project.id),
|
|
24011
|
+
eq25(runs.kind, RunKinds["ads-sync"]),
|
|
22922
24012
|
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
22923
24013
|
)).get();
|
|
22924
24014
|
if (inFlight) {
|
|
@@ -22940,9 +24030,9 @@ async function adsRoutes(app, opts) {
|
|
|
22940
24030
|
});
|
|
22941
24031
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
22942
24032
|
const project = resolveProject(app.db, request.params.name);
|
|
22943
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
22944
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
22945
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
24033
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq25(adsCampaigns.projectId, project.id)).all();
|
|
24034
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq25(adsAdGroups.projectId, project.id)).all();
|
|
24035
|
+
const adRows = app.db.select().from(adsAds).where(eq25(adsAds.projectId, project.id)).all();
|
|
22946
24036
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
22947
24037
|
for (const ad of adRows) {
|
|
22948
24038
|
const dto = {
|
|
@@ -22951,7 +24041,9 @@ async function adsRoutes(app, opts) {
|
|
|
22951
24041
|
name: ad.name,
|
|
22952
24042
|
status: ad.status,
|
|
22953
24043
|
reviewStatus: ad.reviewStatus,
|
|
22954
|
-
creative: creativeDto(ad.creative)
|
|
24044
|
+
creative: creativeDto(ad.creative),
|
|
24045
|
+
upstreamUpdatedAt: ad.upstreamUpdatedAt,
|
|
24046
|
+
syncedAt: ad.syncedAt
|
|
22955
24047
|
};
|
|
22956
24048
|
const list = adsByGroup.get(ad.adGroupId) ?? [];
|
|
22957
24049
|
list.push(dto);
|
|
@@ -22963,11 +24055,14 @@ async function adsRoutes(app, opts) {
|
|
|
22963
24055
|
id: group.id,
|
|
22964
24056
|
campaignId: group.campaignId,
|
|
22965
24057
|
name: group.name,
|
|
24058
|
+
description: group.description,
|
|
22966
24059
|
status: group.status,
|
|
22967
24060
|
billingEventType: group.billingEventType,
|
|
22968
24061
|
maxBidMicros: group.maxBidMicros,
|
|
22969
24062
|
contextHints: group.contextHints,
|
|
22970
|
-
ads: adsByGroup.get(group.id) ?? []
|
|
24063
|
+
ads: adsByGroup.get(group.id) ?? [],
|
|
24064
|
+
upstreamUpdatedAt: group.upstreamUpdatedAt,
|
|
24065
|
+
syncedAt: group.syncedAt
|
|
22971
24066
|
};
|
|
22972
24067
|
const list = groupsByCampaign.get(group.campaignId) ?? [];
|
|
22973
24068
|
list.push(dto);
|
|
@@ -22976,11 +24071,17 @@ async function adsRoutes(app, opts) {
|
|
|
22976
24071
|
const campaigns = campaignRows.map((campaign) => ({
|
|
22977
24072
|
id: campaign.id,
|
|
22978
24073
|
name: campaign.name,
|
|
24074
|
+
description: campaign.description,
|
|
22979
24075
|
status: campaign.status,
|
|
24076
|
+
startTime: campaign.startTime,
|
|
24077
|
+
endTime: campaign.endTime,
|
|
22980
24078
|
biddingType: campaign.biddingType,
|
|
22981
24079
|
dailySpendLimitMicros: campaign.dailySpendLimitMicros,
|
|
22982
24080
|
lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
|
|
22983
|
-
|
|
24081
|
+
locationIds: locationIdsDto(campaign.targeting),
|
|
24082
|
+
adGroups: groupsByCampaign.get(campaign.id) ?? [],
|
|
24083
|
+
upstreamUpdatedAt: campaign.upstreamUpdatedAt,
|
|
24084
|
+
syncedAt: campaign.syncedAt
|
|
22984
24085
|
}));
|
|
22985
24086
|
const response = { campaigns };
|
|
22986
24087
|
return response;
|
|
@@ -22996,12 +24097,12 @@ async function adsRoutes(app, opts) {
|
|
|
22996
24097
|
}
|
|
22997
24098
|
parsedLevel = result.data;
|
|
22998
24099
|
}
|
|
22999
|
-
const conditions = [
|
|
23000
|
-
if (parsedLevel) conditions.push(
|
|
23001
|
-
if (entityId) conditions.push(
|
|
23002
|
-
if (from) conditions.push(
|
|
23003
|
-
if (to) conditions.push(
|
|
23004
|
-
const rows = app.db.select().from(adsInsightsDaily).where(
|
|
24100
|
+
const conditions = [eq25(adsInsightsDaily.projectId, project.id)];
|
|
24101
|
+
if (parsedLevel) conditions.push(eq25(adsInsightsDaily.level, parsedLevel));
|
|
24102
|
+
if (entityId) conditions.push(eq25(adsInsightsDaily.entityId, entityId));
|
|
24103
|
+
if (from) conditions.push(gte5(adsInsightsDaily.date, from));
|
|
24104
|
+
if (to) conditions.push(lte3(adsInsightsDaily.date, to));
|
|
24105
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and19(...conditions)).orderBy(asc5(adsInsightsDaily.date)).all();
|
|
23005
24106
|
const dtoRows = rows.map((row) => ({
|
|
23006
24107
|
level: row.level,
|
|
23007
24108
|
entityId: row.entityId,
|
|
@@ -23013,19 +24114,19 @@ async function adsRoutes(app, opts) {
|
|
|
23013
24114
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
23014
24115
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
23015
24116
|
}));
|
|
23016
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
24117
|
+
const conn = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
23017
24118
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
23018
24119
|
return response;
|
|
23019
24120
|
});
|
|
23020
24121
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
23021
24122
|
const project = resolveProject(app.db, request.params.name);
|
|
23022
|
-
const row = app.db.select().from(adsConnections).where(
|
|
23023
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
23024
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
23025
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
23026
|
-
const campaignInsights = app.db.select().from(adsInsightsDaily).where(
|
|
23027
|
-
|
|
23028
|
-
|
|
24123
|
+
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
24124
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq25(adsCampaigns.projectId, project.id)).all().length;
|
|
24125
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq25(adsAdGroups.projectId, project.id)).all().length;
|
|
24126
|
+
const adCount = app.db.select().from(adsAds).where(eq25(adsAds.projectId, project.id)).all().length;
|
|
24127
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and19(
|
|
24128
|
+
eq25(adsInsightsDaily.projectId, project.id),
|
|
24129
|
+
eq25(adsInsightsDaily.level, "campaign")
|
|
23029
24130
|
)).all();
|
|
23030
24131
|
let impressions = 0;
|
|
23031
24132
|
let clicks = 0;
|
|
@@ -23065,7 +24166,7 @@ async function adsRoutes(app, opts) {
|
|
|
23065
24166
|
|
|
23066
24167
|
// ../api-routes/src/bing.ts
|
|
23067
24168
|
import crypto21 from "crypto";
|
|
23068
|
-
import { eq as
|
|
24169
|
+
import { eq as eq26, and as and20, desc as desc12 } from "drizzle-orm";
|
|
23069
24170
|
|
|
23070
24171
|
// ../integration-bing/src/constants.ts
|
|
23071
24172
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -23439,7 +24540,7 @@ async function bingRoutes(app, opts) {
|
|
|
23439
24540
|
const store = requireConnectionStore();
|
|
23440
24541
|
const project = resolveProject(app.db, request.params.name);
|
|
23441
24542
|
requireConnection(store, project.canonicalDomain);
|
|
23442
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
24543
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq26(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
23443
24544
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
23444
24545
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
23445
24546
|
for (const row of allInspections) {
|
|
@@ -23528,7 +24629,7 @@ async function bingRoutes(app, opts) {
|
|
|
23528
24629
|
const project = resolveProject(app.db, request.params.name);
|
|
23529
24630
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
23530
24631
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
23531
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
24632
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq26(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
23532
24633
|
return rows.map((r) => ({
|
|
23533
24634
|
date: r.date,
|
|
23534
24635
|
indexed: r.indexed,
|
|
@@ -23540,7 +24641,7 @@ async function bingRoutes(app, opts) {
|
|
|
23540
24641
|
requireConnectionStore();
|
|
23541
24642
|
const project = resolveProject(app.db, request.params.name);
|
|
23542
24643
|
const { url, limit } = request.query;
|
|
23543
|
-
const whereClause = url ?
|
|
24644
|
+
const whereClause = url ? and20(eq26(bingUrlInspections.projectId, project.id), eq26(bingUrlInspections.url, url)) : eq26(bingUrlInspections.projectId, project.id);
|
|
23544
24645
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc12(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
23545
24646
|
return filtered.map((r) => ({
|
|
23546
24647
|
id: r.id,
|
|
@@ -23630,7 +24731,7 @@ async function bingRoutes(app, opts) {
|
|
|
23630
24731
|
anchorCount: result.AnchorCount ?? null,
|
|
23631
24732
|
discoveryDate
|
|
23632
24733
|
}).run();
|
|
23633
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
24734
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq26(runs.id, runId)).run();
|
|
23634
24735
|
return {
|
|
23635
24736
|
id,
|
|
23636
24737
|
url,
|
|
@@ -23646,7 +24747,7 @@ async function bingRoutes(app, opts) {
|
|
|
23646
24747
|
} catch (e) {
|
|
23647
24748
|
const msg = e instanceof Error ? e.message : String(e);
|
|
23648
24749
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
23649
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
24750
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
|
|
23650
24751
|
throw e;
|
|
23651
24752
|
}
|
|
23652
24753
|
});
|
|
@@ -23673,7 +24774,7 @@ async function bingRoutes(app, opts) {
|
|
|
23673
24774
|
} else {
|
|
23674
24775
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
23675
24776
|
}
|
|
23676
|
-
const run = app.db.select().from(runs).where(
|
|
24777
|
+
const run = app.db.select().from(runs).where(eq26(runs.id, runId)).get();
|
|
23677
24778
|
return run;
|
|
23678
24779
|
});
|
|
23679
24780
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -23685,7 +24786,7 @@ async function bingRoutes(app, opts) {
|
|
|
23685
24786
|
}
|
|
23686
24787
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
23687
24788
|
if (request.body?.allUnindexed) {
|
|
23688
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
24789
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq26(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
23689
24790
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
23690
24791
|
for (const row of allInspections) {
|
|
23691
24792
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -23772,14 +24873,14 @@ async function bingRoutes(app, opts) {
|
|
|
23772
24873
|
import fs from "fs";
|
|
23773
24874
|
import path from "path";
|
|
23774
24875
|
import os from "os";
|
|
23775
|
-
import { eq as
|
|
24876
|
+
import { eq as eq27, and as and21 } from "drizzle-orm";
|
|
23776
24877
|
function getScreenshotDir() {
|
|
23777
24878
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
23778
24879
|
}
|
|
23779
24880
|
async function cdpRoutes(app, opts) {
|
|
23780
24881
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
23781
24882
|
const { snapshotId } = request.params;
|
|
23782
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs,
|
|
24883
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq27(querySnapshots.runId, runs.id)).where(eq27(querySnapshots.id, snapshotId)).get();
|
|
23783
24884
|
if (!snapshot?.screenshotPath) {
|
|
23784
24885
|
const err = notFound("Screenshot", snapshotId);
|
|
23785
24886
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -23847,7 +24948,7 @@ async function cdpRoutes(app, opts) {
|
|
|
23847
24948
|
async (request, reply) => {
|
|
23848
24949
|
const project = resolveProject(app.db, request.params.name);
|
|
23849
24950
|
const { runId } = request.params;
|
|
23850
|
-
const run = app.db.select().from(runs).where(
|
|
24951
|
+
const run = app.db.select().from(runs).where(and21(eq27(runs.id, runId), eq27(runs.projectId, project.id))).get();
|
|
23851
24952
|
if (!run) {
|
|
23852
24953
|
const err = notFound("Run", runId);
|
|
23853
24954
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -23860,8 +24961,8 @@ async function cdpRoutes(app, opts) {
|
|
|
23860
24961
|
citedDomains: querySnapshots.citedDomains,
|
|
23861
24962
|
screenshotPath: querySnapshots.screenshotPath,
|
|
23862
24963
|
rawResponse: querySnapshots.rawResponse
|
|
23863
|
-
}).from(querySnapshots).where(
|
|
23864
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
24964
|
+
}).from(querySnapshots).where(eq27(querySnapshots.runId, runId)).all());
|
|
24965
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq27(queries.projectId, project.id)).all();
|
|
23865
24966
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
23866
24967
|
const byQuery = /* @__PURE__ */ new Map();
|
|
23867
24968
|
for (const snap of snapshots) {
|
|
@@ -23944,7 +25045,7 @@ async function cdpRoutes(app, opts) {
|
|
|
23944
25045
|
|
|
23945
25046
|
// ../api-routes/src/ga.ts
|
|
23946
25047
|
import crypto22 from "crypto";
|
|
23947
|
-
import { eq as
|
|
25048
|
+
import { eq as eq28, desc as desc13, and as and22, sql as sql10 } from "drizzle-orm";
|
|
23948
25049
|
function gaLog(level, action, ctx) {
|
|
23949
25050
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
23950
25051
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -24221,10 +25322,10 @@ async function ga4Routes(app, opts) {
|
|
|
24221
25322
|
if (!saConn && !oauthConn) {
|
|
24222
25323
|
throw notFound("GA4 connection", project.name);
|
|
24223
25324
|
}
|
|
24224
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
24225
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
24226
|
-
app.db.delete(gaAiReferrals).where(
|
|
24227
|
-
app.db.delete(gaSocialReferrals).where(
|
|
25325
|
+
app.db.delete(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, project.id)).run();
|
|
25326
|
+
app.db.delete(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).run();
|
|
25327
|
+
app.db.delete(gaAiReferrals).where(eq28(gaAiReferrals.projectId, project.id)).run();
|
|
25328
|
+
app.db.delete(gaSocialReferrals).where(eq28(gaSocialReferrals.projectId, project.id)).run();
|
|
24228
25329
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
24229
25330
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
24230
25331
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -24245,7 +25346,7 @@ async function ga4Routes(app, opts) {
|
|
|
24245
25346
|
if (!connected) {
|
|
24246
25347
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
24247
25348
|
}
|
|
24248
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
25349
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
24249
25350
|
return {
|
|
24250
25351
|
connected: true,
|
|
24251
25352
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -24309,8 +25410,8 @@ async function ga4Routes(app, opts) {
|
|
|
24309
25410
|
app.db.transaction((tx) => {
|
|
24310
25411
|
if (syncTraffic) {
|
|
24311
25412
|
tx.delete(gaTrafficSnapshots).where(
|
|
24312
|
-
|
|
24313
|
-
|
|
25413
|
+
and22(
|
|
25414
|
+
eq28(gaTrafficSnapshots.projectId, project.id),
|
|
24314
25415
|
sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
24315
25416
|
sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
24316
25417
|
)
|
|
@@ -24333,8 +25434,8 @@ async function ga4Routes(app, opts) {
|
|
|
24333
25434
|
}
|
|
24334
25435
|
if (syncAi) {
|
|
24335
25436
|
tx.delete(gaAiReferrals).where(
|
|
24336
|
-
|
|
24337
|
-
|
|
25437
|
+
and22(
|
|
25438
|
+
eq28(gaAiReferrals.projectId, project.id),
|
|
24338
25439
|
sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
24339
25440
|
sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
24340
25441
|
)
|
|
@@ -24365,8 +25466,8 @@ async function ga4Routes(app, opts) {
|
|
|
24365
25466
|
}
|
|
24366
25467
|
if (syncSocial) {
|
|
24367
25468
|
tx.delete(gaSocialReferrals).where(
|
|
24368
|
-
|
|
24369
|
-
|
|
25469
|
+
and22(
|
|
25470
|
+
eq28(gaSocialReferrals.projectId, project.id),
|
|
24370
25471
|
sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
24371
25472
|
sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
24372
25473
|
)
|
|
@@ -24387,7 +25488,7 @@ async function ga4Routes(app, opts) {
|
|
|
24387
25488
|
}
|
|
24388
25489
|
}
|
|
24389
25490
|
if (syncSummary) {
|
|
24390
|
-
tx.delete(gaTrafficSummaries).where(
|
|
25491
|
+
tx.delete(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).run();
|
|
24391
25492
|
tx.insert(gaTrafficSummaries).values({
|
|
24392
25493
|
id: crypto22.randomUUID(),
|
|
24393
25494
|
projectId: project.id,
|
|
@@ -24399,7 +25500,7 @@ async function ga4Routes(app, opts) {
|
|
|
24399
25500
|
syncedAt: now,
|
|
24400
25501
|
syncRunId: runId
|
|
24401
25502
|
}).run();
|
|
24402
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
25503
|
+
tx.delete(gaTrafficWindowSummaries).where(eq28(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
24403
25504
|
for (const ws of windowSummaries) {
|
|
24404
25505
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
24405
25506
|
id: crypto22.randomUUID(),
|
|
@@ -24417,7 +25518,7 @@ async function ga4Routes(app, opts) {
|
|
|
24417
25518
|
}
|
|
24418
25519
|
}
|
|
24419
25520
|
});
|
|
24420
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
25521
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq28(runs.id, runId)).run();
|
|
24421
25522
|
const syncedComponents = only ? [
|
|
24422
25523
|
...syncTraffic ? ["traffic"] : [],
|
|
24423
25524
|
...syncSummary ? ["summary"] : [],
|
|
@@ -24446,7 +25547,7 @@ async function ga4Routes(app, opts) {
|
|
|
24446
25547
|
} catch (e) {
|
|
24447
25548
|
const msg = e instanceof Error ? e.message : String(e);
|
|
24448
25549
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
24449
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
25550
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq28(runs.id, runId)).run();
|
|
24450
25551
|
throw e;
|
|
24451
25552
|
}
|
|
24452
25553
|
});
|
|
@@ -24457,11 +25558,11 @@ async function ga4Routes(app, opts) {
|
|
|
24457
25558
|
const window = parseWindow(request.query.window);
|
|
24458
25559
|
const cutoff = windowCutoff(window);
|
|
24459
25560
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
24460
|
-
const snapshotConditions = [
|
|
25561
|
+
const snapshotConditions = [eq28(gaTrafficSnapshots.projectId, project.id)];
|
|
24461
25562
|
if (cutoffDate) snapshotConditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
24462
|
-
const aiConditions = [
|
|
25563
|
+
const aiConditions = [eq28(gaAiReferrals.projectId, project.id)];
|
|
24463
25564
|
if (cutoffDate) aiConditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
24464
|
-
const socialConditions = [
|
|
25565
|
+
const socialConditions = [eq28(gaSocialReferrals.projectId, project.id)];
|
|
24465
25566
|
if (cutoffDate) socialConditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
24466
25567
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
24467
25568
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -24469,35 +25570,35 @@ async function ga4Routes(app, opts) {
|
|
|
24469
25570
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
24470
25571
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
24471
25572
|
}).from(gaTrafficWindowSummaries).where(
|
|
24472
|
-
|
|
24473
|
-
|
|
24474
|
-
|
|
25573
|
+
and22(
|
|
25574
|
+
eq28(gaTrafficWindowSummaries.projectId, project.id),
|
|
25575
|
+
eq28(gaTrafficWindowSummaries.windowKey, window)
|
|
24475
25576
|
)
|
|
24476
25577
|
).get() : null;
|
|
24477
25578
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
24478
25579
|
totalSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
24479
25580
|
totalOrganicSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
24480
25581
|
totalUsers: sql10`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
24481
|
-
}).from(gaTrafficSnapshots).where(
|
|
25582
|
+
}).from(gaTrafficSnapshots).where(and22(...snapshotConditions)).get() : null;
|
|
24482
25583
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
24483
25584
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
24484
25585
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
24485
25586
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
24486
|
-
}).from(gaTrafficSummaries).where(
|
|
25587
|
+
}).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).get();
|
|
24487
25588
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
24488
25589
|
totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
24489
|
-
}).from(gaTrafficSnapshots).where(
|
|
25590
|
+
}).from(gaTrafficSnapshots).where(and22(...snapshotConditions)).get();
|
|
24490
25591
|
const summaryMeta = app.db.select({
|
|
24491
25592
|
periodStart: gaTrafficSummaries.periodStart,
|
|
24492
25593
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
24493
|
-
}).from(gaTrafficSummaries).where(
|
|
25594
|
+
}).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).get();
|
|
24494
25595
|
const rows = app.db.select({
|
|
24495
25596
|
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
24496
25597
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
24497
25598
|
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
24498
25599
|
directSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
24499
25600
|
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
24500
|
-
}).from(gaTrafficSnapshots).where(
|
|
25601
|
+
}).from(gaTrafficSnapshots).where(and22(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
24501
25602
|
const aiReferralRows = app.db.select({
|
|
24502
25603
|
source: gaAiReferrals.source,
|
|
24503
25604
|
medium: gaAiReferrals.medium,
|
|
@@ -24505,7 +25606,7 @@ async function ga4Routes(app, opts) {
|
|
|
24505
25606
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
24506
25607
|
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
24507
25608
|
users: sql10`SUM(${gaAiReferrals.users})`
|
|
24508
|
-
}).from(gaAiReferrals).where(
|
|
25609
|
+
}).from(gaAiReferrals).where(and22(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
|
|
24509
25610
|
const aiReferralLandingPageRows = app.db.select({
|
|
24510
25611
|
source: gaAiReferrals.source,
|
|
24511
25612
|
medium: gaAiReferrals.medium,
|
|
@@ -24514,7 +25615,7 @@ async function ga4Routes(app, opts) {
|
|
|
24514
25615
|
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
24515
25616
|
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
24516
25617
|
users: sql10`SUM(${gaAiReferrals.users})`
|
|
24517
|
-
}).from(gaAiReferrals).where(
|
|
25618
|
+
}).from(gaAiReferrals).where(and22(...aiConditions)).groupBy(
|
|
24518
25619
|
gaAiReferrals.source,
|
|
24519
25620
|
gaAiReferrals.medium,
|
|
24520
25621
|
gaAiReferrals.trafficClass,
|
|
@@ -24530,7 +25631,7 @@ async function ga4Routes(app, opts) {
|
|
|
24530
25631
|
channelGroup: gaAiReferrals.channelGroup,
|
|
24531
25632
|
sessions: gaAiReferrals.sessions,
|
|
24532
25633
|
users: gaAiReferrals.users
|
|
24533
|
-
}).from(gaAiReferrals).where(
|
|
25634
|
+
}).from(gaAiReferrals).where(and22(...aiConditions)).all();
|
|
24534
25635
|
const aiReferrals = pickWinningDimension(
|
|
24535
25636
|
aiReferralRows,
|
|
24536
25637
|
(r) => `${r.source}\0${r.medium}\0${r.trafficClass}`
|
|
@@ -24546,12 +25647,12 @@ async function ga4Routes(app, opts) {
|
|
|
24546
25647
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
24547
25648
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
24548
25649
|
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
24549
|
-
}).from(gaSocialReferrals).where(
|
|
25650
|
+
}).from(gaSocialReferrals).where(and22(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
24550
25651
|
const socialTotals = app.db.select({
|
|
24551
25652
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
24552
25653
|
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
24553
|
-
}).from(gaSocialReferrals).where(
|
|
24554
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
25654
|
+
}).from(gaSocialReferrals).where(and22(...socialConditions)).get();
|
|
25655
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
24555
25656
|
const total = summaryRow?.totalSessions ?? 0;
|
|
24556
25657
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
24557
25658
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -24649,7 +25750,7 @@ async function ga4Routes(app, opts) {
|
|
|
24649
25750
|
const project = resolveProject(app.db, request.params.name);
|
|
24650
25751
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24651
25752
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24652
|
-
const conditions = [
|
|
25753
|
+
const conditions = [eq28(gaAiReferrals.projectId, project.id)];
|
|
24653
25754
|
if (cutoffDate) conditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
24654
25755
|
const rows = app.db.select({
|
|
24655
25756
|
date: gaAiReferrals.date,
|
|
@@ -24660,7 +25761,7 @@ async function ga4Routes(app, opts) {
|
|
|
24660
25761
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
24661
25762
|
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
24662
25763
|
users: sql10`SUM(${gaAiReferrals.users})`
|
|
24663
|
-
}).from(gaAiReferrals).where(
|
|
25764
|
+
}).from(gaAiReferrals).where(and22(...conditions)).groupBy(
|
|
24664
25765
|
gaAiReferrals.date,
|
|
24665
25766
|
gaAiReferrals.source,
|
|
24666
25767
|
gaAiReferrals.medium,
|
|
@@ -24674,7 +25775,7 @@ async function ga4Routes(app, opts) {
|
|
|
24674
25775
|
const project = resolveProject(app.db, request.params.name);
|
|
24675
25776
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24676
25777
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24677
|
-
const conditions = [
|
|
25778
|
+
const conditions = [eq28(gaSocialReferrals.projectId, project.id)];
|
|
24678
25779
|
if (cutoffDate) conditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
24679
25780
|
const rows = app.db.select({
|
|
24680
25781
|
date: gaSocialReferrals.date,
|
|
@@ -24683,7 +25784,7 @@ async function ga4Routes(app, opts) {
|
|
|
24683
25784
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
24684
25785
|
sessions: gaSocialReferrals.sessions,
|
|
24685
25786
|
users: gaSocialReferrals.users
|
|
24686
|
-
}).from(gaSocialReferrals).where(
|
|
25787
|
+
}).from(gaSocialReferrals).where(and22(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
24687
25788
|
return rows;
|
|
24688
25789
|
});
|
|
24689
25790
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -24696,8 +25797,8 @@ async function ga4Routes(app, opts) {
|
|
|
24696
25797
|
d.setDate(d.getDate() - n);
|
|
24697
25798
|
return fmt(d);
|
|
24698
25799
|
};
|
|
24699
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
24700
|
-
|
|
25800
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and22(
|
|
25801
|
+
eq28(gaSocialReferrals.projectId, project.id),
|
|
24701
25802
|
sql10`${gaSocialReferrals.date} >= ${from}`,
|
|
24702
25803
|
sql10`${gaSocialReferrals.date} < ${to}`
|
|
24703
25804
|
)).get();
|
|
@@ -24709,16 +25810,16 @@ async function ga4Routes(app, opts) {
|
|
|
24709
25810
|
const sourceCurrent = app.db.select({
|
|
24710
25811
|
source: gaSocialReferrals.source,
|
|
24711
25812
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
24712
|
-
}).from(gaSocialReferrals).where(
|
|
24713
|
-
|
|
25813
|
+
}).from(gaSocialReferrals).where(and22(
|
|
25814
|
+
eq28(gaSocialReferrals.projectId, project.id),
|
|
24714
25815
|
sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
24715
25816
|
sql10`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
24716
25817
|
)).groupBy(gaSocialReferrals.source).all();
|
|
24717
25818
|
const sourcePrev = app.db.select({
|
|
24718
25819
|
source: gaSocialReferrals.source,
|
|
24719
25820
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
24720
|
-
}).from(gaSocialReferrals).where(
|
|
24721
|
-
|
|
25821
|
+
}).from(gaSocialReferrals).where(and22(
|
|
25822
|
+
eq28(gaSocialReferrals.projectId, project.id),
|
|
24722
25823
|
sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
24723
25824
|
sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
24724
25825
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -24759,16 +25860,16 @@ async function ga4Routes(app, opts) {
|
|
|
24759
25860
|
return fmt(d);
|
|
24760
25861
|
};
|
|
24761
25862
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
24762
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
24763
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
24764
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
24765
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
24766
|
-
|
|
25863
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and22(eq28(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
25864
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and22(eq28(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
25865
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and22(eq28(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
25866
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and22(
|
|
25867
|
+
eq28(gaAiReferrals.projectId, project.id),
|
|
24767
25868
|
sql10`${gaAiReferrals.date} >= ${from}`,
|
|
24768
25869
|
sql10`${gaAiReferrals.date} < ${to}`,
|
|
24769
|
-
|
|
25870
|
+
eq28(gaAiReferrals.sourceDimension, "session")
|
|
24770
25871
|
)).get();
|
|
24771
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
25872
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and22(eq28(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
|
|
24772
25873
|
const todayStr = fmt(today);
|
|
24773
25874
|
const buildTrend = (sum) => {
|
|
24774
25875
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -24777,17 +25878,17 @@ async function ga4Routes(app, opts) {
|
|
|
24777
25878
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
24778
25879
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
24779
25880
|
};
|
|
24780
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
24781
|
-
|
|
25881
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and22(
|
|
25882
|
+
eq28(gaAiReferrals.projectId, project.id),
|
|
24782
25883
|
sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
24783
25884
|
sql10`${gaAiReferrals.date} < ${todayStr}`,
|
|
24784
|
-
|
|
25885
|
+
eq28(gaAiReferrals.sourceDimension, "session")
|
|
24785
25886
|
)).groupBy(gaAiReferrals.source).all();
|
|
24786
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
24787
|
-
|
|
25887
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and22(
|
|
25888
|
+
eq28(gaAiReferrals.projectId, project.id),
|
|
24788
25889
|
sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
24789
25890
|
sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
24790
|
-
|
|
25891
|
+
eq28(gaAiReferrals.sourceDimension, "session")
|
|
24791
25892
|
)).groupBy(gaAiReferrals.source).all();
|
|
24792
25893
|
const findBiggestMover = (current, prev) => {
|
|
24793
25894
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -24803,8 +25904,8 @@ async function ga4Routes(app, opts) {
|
|
|
24803
25904
|
}
|
|
24804
25905
|
return mover;
|
|
24805
25906
|
};
|
|
24806
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
24807
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
25907
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and22(eq28(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
25908
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and22(eq28(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
24808
25909
|
return {
|
|
24809
25910
|
total: buildTrend(sumTotal),
|
|
24810
25911
|
organic: buildTrend(sumOrganic),
|
|
@@ -24819,14 +25920,14 @@ async function ga4Routes(app, opts) {
|
|
|
24819
25920
|
const project = resolveProject(app.db, request.params.name);
|
|
24820
25921
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24821
25922
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24822
|
-
const conditions = [
|
|
25923
|
+
const conditions = [eq28(gaTrafficSnapshots.projectId, project.id)];
|
|
24823
25924
|
if (cutoffDate) conditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
24824
25925
|
const rows = app.db.select({
|
|
24825
25926
|
date: gaTrafficSnapshots.date,
|
|
24826
25927
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
24827
25928
|
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
24828
25929
|
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
24829
|
-
}).from(gaTrafficSnapshots).where(
|
|
25930
|
+
}).from(gaTrafficSnapshots).where(and22(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
24830
25931
|
return rows.map((r) => ({
|
|
24831
25932
|
date: r.date,
|
|
24832
25933
|
sessions: r.sessions ?? 0,
|
|
@@ -24842,7 +25943,7 @@ async function ga4Routes(app, opts) {
|
|
|
24842
25943
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
24843
25944
|
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
24844
25945
|
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
24845
|
-
}).from(gaTrafficSnapshots).where(
|
|
25946
|
+
}).from(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
24846
25947
|
return {
|
|
24847
25948
|
pages: trafficPages.map((r) => ({
|
|
24848
25949
|
landingPage: r.landingPage,
|
|
@@ -26490,7 +27591,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
26490
27591
|
|
|
26491
27592
|
// ../api-routes/src/backlinks.ts
|
|
26492
27593
|
import crypto24 from "crypto";
|
|
26493
|
-
import { and as
|
|
27594
|
+
import { and as and24, asc as asc6, desc as desc14, eq as eq29, sql as sql11 } from "drizzle-orm";
|
|
26494
27595
|
|
|
26495
27596
|
// ../integration-commoncrawl/src/constants.ts
|
|
26496
27597
|
import os2 from "os";
|
|
@@ -26893,7 +27994,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
26893
27994
|
}
|
|
26894
27995
|
|
|
26895
27996
|
// ../api-routes/src/backlinks-filter.ts
|
|
26896
|
-
import { and as
|
|
27997
|
+
import { and as and23, ne as ne3, notLike } from "drizzle-orm";
|
|
26897
27998
|
var BACKLINK_FILTER_PATTERNS = [
|
|
26898
27999
|
"*.google.com",
|
|
26899
28000
|
"*.googleusercontent.com",
|
|
@@ -26916,7 +28017,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
26916
28017
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
26917
28018
|
}
|
|
26918
28019
|
}
|
|
26919
|
-
const combined =
|
|
28020
|
+
const combined = and23(...conditions);
|
|
26920
28021
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
26921
28022
|
return combined;
|
|
26922
28023
|
}
|
|
@@ -26987,11 +28088,11 @@ function mapRunRow(row) {
|
|
|
26987
28088
|
}
|
|
26988
28089
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
26989
28090
|
const conditions = [
|
|
26990
|
-
|
|
26991
|
-
|
|
28091
|
+
eq29(backlinkSummaries.projectId, projectId),
|
|
28092
|
+
eq29(backlinkSummaries.source, source)
|
|
26992
28093
|
];
|
|
26993
|
-
if (release) conditions.push(
|
|
26994
|
-
return db.select().from(backlinkSummaries).where(
|
|
28094
|
+
if (release) conditions.push(eq29(backlinkSummaries.release, release));
|
|
28095
|
+
return db.select().from(backlinkSummaries).where(and24(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
26995
28096
|
}
|
|
26996
28097
|
function parseExcludeCrawlers(value) {
|
|
26997
28098
|
if (!value) return false;
|
|
@@ -26999,12 +28100,12 @@ function parseExcludeCrawlers(value) {
|
|
|
26999
28100
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
27000
28101
|
}
|
|
27001
28102
|
function computeFilteredSummary(db, base) {
|
|
27002
|
-
const baseDomainCondition =
|
|
27003
|
-
|
|
27004
|
-
|
|
27005
|
-
|
|
28103
|
+
const baseDomainCondition = and24(
|
|
28104
|
+
eq29(backlinkDomains.projectId, base.projectId),
|
|
28105
|
+
eq29(backlinkDomains.source, base.source),
|
|
28106
|
+
eq29(backlinkDomains.release, base.release)
|
|
27006
28107
|
);
|
|
27007
|
-
const filteredCondition =
|
|
28108
|
+
const filteredCondition = and24(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
27008
28109
|
const unfilteredAgg = db.select({
|
|
27009
28110
|
count: sql11`count(*)`,
|
|
27010
28111
|
total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -27034,13 +28135,13 @@ function computeFilteredSummary(db, base) {
|
|
|
27034
28135
|
};
|
|
27035
28136
|
}
|
|
27036
28137
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
27037
|
-
const summary = db.select().from(backlinkSummaries).where(
|
|
28138
|
+
const summary = db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, projectId), eq29(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
27038
28139
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
27039
28140
|
if (summary && excludeCrawlers) {
|
|
27040
|
-
const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(
|
|
27041
|
-
|
|
27042
|
-
|
|
27043
|
-
|
|
28141
|
+
const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and24(
|
|
28142
|
+
eq29(backlinkDomains.projectId, projectId),
|
|
28143
|
+
eq29(backlinkDomains.source, source),
|
|
28144
|
+
eq29(backlinkDomains.release, summary.release),
|
|
27044
28145
|
backlinkCrawlerExclusionClause()
|
|
27045
28146
|
)).get();
|
|
27046
28147
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -27055,7 +28156,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
27055
28156
|
};
|
|
27056
28157
|
}
|
|
27057
28158
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
27058
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
28159
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
27059
28160
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
27060
28161
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
27061
28162
|
const sources = [
|
|
@@ -27111,7 +28212,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27111
28212
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
27112
28213
|
);
|
|
27113
28214
|
}
|
|
27114
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
28215
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, release)).get();
|
|
27115
28216
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
27116
28217
|
if (existing) {
|
|
27117
28218
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -27122,9 +28223,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
27122
28223
|
phaseDetail: null,
|
|
27123
28224
|
error: null,
|
|
27124
28225
|
updatedAt: now
|
|
27125
|
-
}).where(
|
|
28226
|
+
}).where(eq29(ccReleaseSyncs.id, existing.id)).run();
|
|
27126
28227
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
27127
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
28228
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.id, existing.id)).get();
|
|
27128
28229
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
27129
28230
|
}
|
|
27130
28231
|
const id = crypto24.randomUUID();
|
|
@@ -27136,7 +28237,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27136
28237
|
updatedAt: now
|
|
27137
28238
|
}).run();
|
|
27138
28239
|
opts.onReleaseSyncRequested(id, release);
|
|
27139
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
28240
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.id, id)).get();
|
|
27140
28241
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
27141
28242
|
});
|
|
27142
28243
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -27194,7 +28295,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27194
28295
|
createdAt: now
|
|
27195
28296
|
}).run();
|
|
27196
28297
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
27197
|
-
const run = app.db.select().from(runs).where(
|
|
28298
|
+
const run = app.db.select().from(runs).where(eq29(runs.id, runId)).get();
|
|
27198
28299
|
return reply.status(201).send(mapRunRow(run));
|
|
27199
28300
|
});
|
|
27200
28301
|
app.get(
|
|
@@ -27220,12 +28321,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
27220
28321
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
27221
28322
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
27222
28323
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
27223
|
-
const baseDomainCondition =
|
|
27224
|
-
|
|
27225
|
-
|
|
27226
|
-
|
|
28324
|
+
const baseDomainCondition = and24(
|
|
28325
|
+
eq29(backlinkDomains.projectId, project.id),
|
|
28326
|
+
eq29(backlinkDomains.source, source),
|
|
28327
|
+
eq29(backlinkDomains.release, targetRelease)
|
|
27227
28328
|
);
|
|
27228
|
-
const domainCondition = excludeCrawlers ?
|
|
28329
|
+
const domainCondition = excludeCrawlers ? and24(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
27229
28330
|
const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
27230
28331
|
const rows = app.db.select({
|
|
27231
28332
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
@@ -27249,7 +28350,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27249
28350
|
async (request, reply) => {
|
|
27250
28351
|
const project = resolveProject(app.db, request.params.name);
|
|
27251
28352
|
const source = parseSourceParam(request.query.source);
|
|
27252
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
28353
|
+
const rows = app.db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, project.id), eq29(backlinkSummaries.source, source))).orderBy(asc6(backlinkSummaries.queriedAt)).all();
|
|
27253
28354
|
const response = rows.map((r) => ({
|
|
27254
28355
|
release: r.release,
|
|
27255
28356
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -27296,7 +28397,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27296
28397
|
createdAt: now
|
|
27297
28398
|
}).run();
|
|
27298
28399
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
27299
|
-
const run = app.db.select().from(runs).where(
|
|
28400
|
+
const run = app.db.select().from(runs).where(eq29(runs.id, runId)).get();
|
|
27300
28401
|
return reply.status(201).send(mapRunRow(run));
|
|
27301
28402
|
}
|
|
27302
28403
|
);
|
|
@@ -27305,7 +28406,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27305
28406
|
// ../api-routes/src/traffic.ts
|
|
27306
28407
|
import crypto26 from "crypto";
|
|
27307
28408
|
import { Agent as UndiciAgent } from "undici";
|
|
27308
|
-
import { and as
|
|
28409
|
+
import { and as and25, desc as desc15, eq as eq30, gte as gte6, lte as lte4, sql as sql12 } from "drizzle-orm";
|
|
27309
28410
|
|
|
27310
28411
|
// ../integration-cloud-run/src/auth.ts
|
|
27311
28412
|
import crypto25 from "crypto";
|
|
@@ -31172,8 +32273,8 @@ async function runBackfillTask(options) {
|
|
|
31172
32273
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31173
32274
|
try {
|
|
31174
32275
|
app.db.transaction((tx) => {
|
|
31175
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
31176
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
32276
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq30(runs.id, runId)).run();
|
|
32277
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq30(trafficSources.id, sourceRow.id)).run();
|
|
31177
32278
|
});
|
|
31178
32279
|
} catch {
|
|
31179
32280
|
}
|
|
@@ -31188,7 +32289,7 @@ async function runBackfillTask(options) {
|
|
|
31188
32289
|
if (allEvents.length === 0) {
|
|
31189
32290
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
31190
32291
|
try {
|
|
31191
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
32292
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq30(runs.id, runId)).run();
|
|
31192
32293
|
} catch {
|
|
31193
32294
|
}
|
|
31194
32295
|
return;
|
|
@@ -31210,31 +32311,31 @@ async function runBackfillTask(options) {
|
|
|
31210
32311
|
try {
|
|
31211
32312
|
app.db.transaction((tx) => {
|
|
31212
32313
|
tx.delete(crawlerEventsHourly).where(
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
|
|
31216
|
-
|
|
32314
|
+
and25(
|
|
32315
|
+
eq30(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
32316
|
+
gte6(crawlerEventsHourly.tsHour, windowStartIso),
|
|
32317
|
+
lte4(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31217
32318
|
)
|
|
31218
32319
|
).run();
|
|
31219
32320
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
32321
|
+
and25(
|
|
32322
|
+
eq30(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
32323
|
+
gte6(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
32324
|
+
lte4(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
31224
32325
|
)
|
|
31225
32326
|
).run();
|
|
31226
32327
|
tx.delete(aiReferralEventsHourly).where(
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
32328
|
+
and25(
|
|
32329
|
+
eq30(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
32330
|
+
gte6(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
32331
|
+
lte4(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
31231
32332
|
)
|
|
31232
32333
|
).run();
|
|
31233
32334
|
tx.delete(rawEventSamples).where(
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
32335
|
+
and25(
|
|
32336
|
+
eq30(rawEventSamples.sourceId, sourceRow.id),
|
|
32337
|
+
gte6(rawEventSamples.ts, windowStartIso),
|
|
32338
|
+
lte4(rawEventSamples.ts, windowEndIso)
|
|
31238
32339
|
)
|
|
31239
32340
|
).run();
|
|
31240
32341
|
for (const bucket of report.crawlerEventsHourly) {
|
|
@@ -31323,8 +32424,8 @@ async function runBackfillTask(options) {
|
|
|
31323
32424
|
lastError: null,
|
|
31324
32425
|
lastEventIds: newRingBuffer,
|
|
31325
32426
|
updatedAt: finishedAt
|
|
31326
|
-
}).where(
|
|
31327
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
32427
|
+
}).where(eq30(trafficSources.id, sourceRow.id)).run();
|
|
32428
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq30(runs.id, runId)).run();
|
|
31328
32429
|
});
|
|
31329
32430
|
} catch (e) {
|
|
31330
32431
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -31406,7 +32507,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31406
32507
|
createdAt: existing?.createdAt ?? now,
|
|
31407
32508
|
updatedAt: now
|
|
31408
32509
|
});
|
|
31409
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
32510
|
+
const activeSource = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
31410
32511
|
const config = {
|
|
31411
32512
|
gcpProjectId,
|
|
31412
32513
|
serviceName: serviceName ?? null,
|
|
@@ -31422,8 +32523,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31422
32523
|
lastError: null,
|
|
31423
32524
|
configJson: config,
|
|
31424
32525
|
updatedAt: now
|
|
31425
|
-
}).where(
|
|
31426
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
32526
|
+
}).where(eq30(trafficSources.id, activeSource.id)).run();
|
|
32527
|
+
sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, activeSource.id)).get();
|
|
31427
32528
|
} else {
|
|
31428
32529
|
const newId = crypto26.randomUUID();
|
|
31429
32530
|
app.db.insert(trafficSources).values({
|
|
@@ -31440,7 +32541,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31440
32541
|
createdAt: now,
|
|
31441
32542
|
updatedAt: now
|
|
31442
32543
|
}).run();
|
|
31443
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
32544
|
+
sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, newId)).get();
|
|
31444
32545
|
}
|
|
31445
32546
|
writeAuditLog(app.db, {
|
|
31446
32547
|
projectId: project.id,
|
|
@@ -31492,7 +32593,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31492
32593
|
createdAt: existing?.createdAt ?? now,
|
|
31493
32594
|
updatedAt: now
|
|
31494
32595
|
});
|
|
31495
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
32596
|
+
const activeSource = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
31496
32597
|
const config = { baseUrl, username };
|
|
31497
32598
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
31498
32599
|
let sourceRow;
|
|
@@ -31503,8 +32604,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31503
32604
|
lastError: null,
|
|
31504
32605
|
configJson: config,
|
|
31505
32606
|
updatedAt: now
|
|
31506
|
-
}).where(
|
|
31507
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
32607
|
+
}).where(eq30(trafficSources.id, activeSource.id)).run();
|
|
32608
|
+
sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, activeSource.id)).get();
|
|
31508
32609
|
} else {
|
|
31509
32610
|
const newId = crypto26.randomUUID();
|
|
31510
32611
|
app.db.insert(trafficSources).values({
|
|
@@ -31521,7 +32622,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31521
32622
|
createdAt: now,
|
|
31522
32623
|
updatedAt: now
|
|
31523
32624
|
}).run();
|
|
31524
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
32625
|
+
sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, newId)).get();
|
|
31525
32626
|
}
|
|
31526
32627
|
writeAuditLog(app.db, {
|
|
31527
32628
|
projectId: project.id,
|
|
@@ -31575,7 +32676,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31575
32676
|
createdAt: existing?.createdAt ?? now,
|
|
31576
32677
|
updatedAt: now
|
|
31577
32678
|
});
|
|
31578
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
32679
|
+
const activeSource = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
31579
32680
|
const config = { projectId, teamId, environment };
|
|
31580
32681
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
31581
32682
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -31587,8 +32688,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31587
32688
|
lastError: null,
|
|
31588
32689
|
configJson: config,
|
|
31589
32690
|
updatedAt: now
|
|
31590
|
-
}).where(
|
|
31591
|
-
row = tx.select().from(trafficSources).where(
|
|
32691
|
+
}).where(eq30(trafficSources.id, activeSource.id)).run();
|
|
32692
|
+
row = tx.select().from(trafficSources).where(eq30(trafficSources.id, activeSource.id)).get();
|
|
31592
32693
|
} else {
|
|
31593
32694
|
const newId = crypto26.randomUUID();
|
|
31594
32695
|
tx.insert(trafficSources).values({
|
|
@@ -31613,12 +32714,12 @@ async function trafficRoutes(app, opts) {
|
|
|
31613
32714
|
createdAt: now,
|
|
31614
32715
|
updatedAt: now
|
|
31615
32716
|
}).run();
|
|
31616
|
-
row = tx.select().from(trafficSources).where(
|
|
32717
|
+
row = tx.select().from(trafficSources).where(eq30(trafficSources.id, newId)).get();
|
|
31617
32718
|
}
|
|
31618
32719
|
const existingSchedule = tx.select().from(schedules).where(
|
|
31619
|
-
|
|
31620
|
-
|
|
31621
|
-
|
|
32720
|
+
and25(
|
|
32721
|
+
eq30(schedules.projectId, project.id),
|
|
32722
|
+
eq30(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
31622
32723
|
)
|
|
31623
32724
|
).get();
|
|
31624
32725
|
let created = false;
|
|
@@ -31667,7 +32768,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31667
32768
|
});
|
|
31668
32769
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
31669
32770
|
const project = resolveProject(app.db, request.params.name);
|
|
31670
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
32771
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, request.params.id)).get();
|
|
31671
32772
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
31672
32773
|
throw notFound("Traffic source", request.params.id);
|
|
31673
32774
|
}
|
|
@@ -31693,8 +32794,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31693
32794
|
const markFailed = (msg, errorCode) => {
|
|
31694
32795
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31695
32796
|
app.db.transaction((tx) => {
|
|
31696
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
31697
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
32797
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq30(runs.id, runId)).run();
|
|
32798
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq30(trafficSources.id, sourceRow.id)).run();
|
|
31698
32799
|
});
|
|
31699
32800
|
try {
|
|
31700
32801
|
opts.onTrafficSynced?.({
|
|
@@ -31775,7 +32876,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31775
32876
|
}
|
|
31776
32877
|
const credential = credentialStore.getConnection(project.name);
|
|
31777
32878
|
if (!credential) {
|
|
31778
|
-
app.db.delete(runs).where(
|
|
32879
|
+
app.db.delete(runs).where(eq30(runs.id, runId)).run();
|
|
31779
32880
|
throw validationError(
|
|
31780
32881
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
31781
32882
|
);
|
|
@@ -31824,12 +32925,12 @@ async function trafficRoutes(app, opts) {
|
|
|
31824
32925
|
auditAction = "traffic.vercel.synced";
|
|
31825
32926
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
31826
32927
|
if (!credentialStore) {
|
|
31827
|
-
app.db.delete(runs).where(
|
|
32928
|
+
app.db.delete(runs).where(eq30(runs.id, runId)).run();
|
|
31828
32929
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
31829
32930
|
}
|
|
31830
32931
|
const credential = credentialStore.getConnection(project.name);
|
|
31831
32932
|
if (!credential) {
|
|
31832
|
-
app.db.delete(runs).where(
|
|
32933
|
+
app.db.delete(runs).where(eq30(runs.id, runId)).run();
|
|
31833
32934
|
throw validationError(
|
|
31834
32935
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
31835
32936
|
);
|
|
@@ -31929,7 +33030,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31929
33030
|
let aiReferralHitsCount = 0;
|
|
31930
33031
|
let unknownHitsCount = 0;
|
|
31931
33032
|
app.db.transaction((tx) => {
|
|
31932
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
33033
|
+
const latestRow = tx.select().from(trafficSources).where(eq30(trafficSources.id, sourceRow.id)).get();
|
|
31933
33034
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
31934
33035
|
const seenEventIds = new Set(previousIds);
|
|
31935
33036
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -32104,8 +33205,8 @@ async function trafficRoutes(app, opts) {
|
|
|
32104
33205
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
32105
33206
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
32106
33207
|
}
|
|
32107
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
32108
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
33208
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq30(trafficSources.id, sourceRow.id)).run();
|
|
33209
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq30(runs.id, runId)).run();
|
|
32109
33210
|
});
|
|
32110
33211
|
writeAuditLog(app.db, {
|
|
32111
33212
|
projectId: project.id,
|
|
@@ -32157,7 +33258,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32157
33258
|
});
|
|
32158
33259
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
32159
33260
|
const project = resolveProject(app.db, request.params.name);
|
|
32160
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
33261
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, request.params.id)).get();
|
|
32161
33262
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
32162
33263
|
throw notFound("Traffic source", request.params.id);
|
|
32163
33264
|
}
|
|
@@ -32345,9 +33446,9 @@ async function trafficRoutes(app, opts) {
|
|
|
32345
33446
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
32346
33447
|
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
32347
33448
|
}).from(crawlerEventsHourly).where(
|
|
32348
|
-
|
|
32349
|
-
|
|
32350
|
-
|
|
33449
|
+
and25(
|
|
33450
|
+
eq30(crawlerEventsHourly.sourceId, row.id),
|
|
33451
|
+
gte6(crawlerEventsHourly.tsHour, since)
|
|
32351
33452
|
)
|
|
32352
33453
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
32353
33454
|
const crawlerSegments = segmentCrawlerHits(
|
|
@@ -32355,28 +33456,28 @@ async function trafficRoutes(app, opts) {
|
|
|
32355
33456
|
);
|
|
32356
33457
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
32357
33458
|
const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
32358
|
-
|
|
32359
|
-
|
|
32360
|
-
|
|
33459
|
+
and25(
|
|
33460
|
+
eq30(aiUserFetchEventsHourly.sourceId, row.id),
|
|
33461
|
+
gte6(aiUserFetchEventsHourly.tsHour, since)
|
|
32361
33462
|
)
|
|
32362
33463
|
).get();
|
|
32363
33464
|
const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32364
|
-
|
|
32365
|
-
|
|
32366
|
-
|
|
33465
|
+
and25(
|
|
33466
|
+
eq30(aiReferralEventsHourly.sourceId, row.id),
|
|
33467
|
+
gte6(aiReferralEventsHourly.tsHour, since)
|
|
32367
33468
|
)
|
|
32368
33469
|
).get();
|
|
32369
33470
|
const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
|
|
32370
|
-
|
|
32371
|
-
|
|
32372
|
-
|
|
33471
|
+
and25(
|
|
33472
|
+
eq30(rawEventSamples.sourceId, row.id),
|
|
33473
|
+
gte6(rawEventSamples.ts, since)
|
|
32373
33474
|
)
|
|
32374
33475
|
).get();
|
|
32375
33476
|
const latestRun = app.db.select().from(runs).where(
|
|
32376
|
-
|
|
32377
|
-
|
|
32378
|
-
|
|
32379
|
-
|
|
33477
|
+
and25(
|
|
33478
|
+
eq30(runs.projectId, projectId),
|
|
33479
|
+
eq30(runs.kind, RunKinds["traffic-sync"]),
|
|
33480
|
+
eq30(runs.sourceId, row.id)
|
|
32380
33481
|
)
|
|
32381
33482
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
32382
33483
|
return {
|
|
@@ -32407,7 +33508,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32407
33508
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
32408
33509
|
);
|
|
32409
33510
|
}
|
|
32410
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
33511
|
+
const sourceRow = app.db.select().from(trafficSources).where(and25(eq30(trafficSources.projectId, project.id), eq30(trafficSources.id, request.params.id))).get();
|
|
32411
33512
|
if (!sourceRow) {
|
|
32412
33513
|
throw notFound("traffic source", request.params.id);
|
|
32413
33514
|
}
|
|
@@ -32424,7 +33525,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32424
33525
|
status: TrafficSourceStatuses.connected,
|
|
32425
33526
|
lastError: null,
|
|
32426
33527
|
updatedAt: now
|
|
32427
|
-
}).where(
|
|
33528
|
+
}).where(eq30(trafficSources.id, sourceRow.id)).run();
|
|
32428
33529
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
32429
33530
|
projectId: project.id,
|
|
32430
33531
|
actor: "api",
|
|
@@ -32432,20 +33533,20 @@ async function trafficRoutes(app, opts) {
|
|
|
32432
33533
|
entityType: "traffic_source",
|
|
32433
33534
|
entityId: sourceRow.id
|
|
32434
33535
|
}));
|
|
32435
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
33536
|
+
updatedRow = tx.select().from(trafficSources).where(eq30(trafficSources.id, sourceRow.id)).get();
|
|
32436
33537
|
});
|
|
32437
33538
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
32438
33539
|
});
|
|
32439
33540
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
32440
33541
|
const project = resolveProject(app.db, request.params.name);
|
|
32441
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
33542
|
+
const rows = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
32442
33543
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
32443
33544
|
const response = { sources };
|
|
32444
33545
|
return response;
|
|
32445
33546
|
});
|
|
32446
33547
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
32447
33548
|
const project = resolveProject(app.db, request.params.name);
|
|
32448
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
33549
|
+
const rows = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
32449
33550
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
32450
33551
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
32451
33552
|
const response = { sources };
|
|
@@ -32455,7 +33556,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32455
33556
|
"/projects/:name/traffic/sources/:id",
|
|
32456
33557
|
async (request) => {
|
|
32457
33558
|
const project = resolveProject(app.db, request.params.name);
|
|
32458
|
-
const row = app.db.select().from(trafficSources).where(
|
|
33559
|
+
const row = app.db.select().from(trafficSources).where(eq30(trafficSources.id, request.params.id)).get();
|
|
32459
33560
|
if (!row || row.projectId !== project.id) {
|
|
32460
33561
|
throw notFound("Traffic source", request.params.id);
|
|
32461
33562
|
}
|
|
@@ -32507,12 +33608,12 @@ async function trafficRoutes(app, opts) {
|
|
|
32507
33608
|
let aiReferralCounts = aiReferralClassCounts(0, 0, 0);
|
|
32508
33609
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
32509
33610
|
const crawlerFilters = [
|
|
32510
|
-
|
|
32511
|
-
|
|
32512
|
-
|
|
33611
|
+
eq30(crawlerEventsHourly.projectId, project.id),
|
|
33612
|
+
gte6(crawlerEventsHourly.tsHour, sinceIso),
|
|
33613
|
+
lte4(crawlerEventsHourly.tsHour, untilIso)
|
|
32513
33614
|
];
|
|
32514
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
32515
|
-
const crawlerWhere =
|
|
33615
|
+
if (sourceIdParam) crawlerFilters.push(eq30(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
33616
|
+
const crawlerWhere = and25(...crawlerFilters);
|
|
32516
33617
|
const pathTotals = app.db.select({
|
|
32517
33618
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
32518
33619
|
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
@@ -32539,12 +33640,12 @@ async function trafficRoutes(app, opts) {
|
|
|
32539
33640
|
}
|
|
32540
33641
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
32541
33642
|
const userFetchFilters = [
|
|
32542
|
-
|
|
32543
|
-
|
|
32544
|
-
|
|
33643
|
+
eq30(aiUserFetchEventsHourly.projectId, project.id),
|
|
33644
|
+
gte6(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
33645
|
+
lte4(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
32545
33646
|
];
|
|
32546
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
32547
|
-
const userFetchWhere =
|
|
33647
|
+
if (sourceIdParam) userFetchFilters.push(eq30(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
33648
|
+
const userFetchWhere = and25(...userFetchFilters);
|
|
32548
33649
|
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
32549
33650
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
32550
33651
|
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
@@ -32564,12 +33665,12 @@ async function trafficRoutes(app, opts) {
|
|
|
32564
33665
|
}
|
|
32565
33666
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
32566
33667
|
const aiFilters = [
|
|
32567
|
-
|
|
32568
|
-
|
|
32569
|
-
|
|
33668
|
+
eq30(aiReferralEventsHourly.projectId, project.id),
|
|
33669
|
+
gte6(aiReferralEventsHourly.tsHour, sinceIso),
|
|
33670
|
+
lte4(aiReferralEventsHourly.tsHour, untilIso)
|
|
32570
33671
|
];
|
|
32571
|
-
if (sourceIdParam) aiFilters.push(
|
|
32572
|
-
const aiWhere =
|
|
33672
|
+
if (sourceIdParam) aiFilters.push(eq30(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
33673
|
+
const aiWhere = and25(...aiFilters);
|
|
32573
33674
|
const total = app.db.select({
|
|
32574
33675
|
total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
32575
33676
|
paid: sql12`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
|
|
@@ -32791,7 +33892,7 @@ function readInstalledManifest(skillDir) {
|
|
|
32791
33892
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
32792
33893
|
|
|
32793
33894
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
32794
|
-
import { and as
|
|
33895
|
+
import { and as and26, eq as eq31 } from "drizzle-orm";
|
|
32795
33896
|
function skippedNoProject() {
|
|
32796
33897
|
return {
|
|
32797
33898
|
status: CheckStatuses.skipped,
|
|
@@ -32807,8 +33908,8 @@ var BACKLINKS_CHECKS = [
|
|
|
32807
33908
|
title: "Backlinks source connected",
|
|
32808
33909
|
run: (ctx) => {
|
|
32809
33910
|
if (!ctx.project) return skippedNoProject();
|
|
32810
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
32811
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
33911
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq31(projects.id, ctx.project.id)).get();
|
|
33912
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
32812
33913
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
32813
33914
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
32814
33915
|
const connected = [];
|
|
@@ -32823,9 +33924,9 @@ var BACKLINKS_CHECKS = [
|
|
|
32823
33924
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
32824
33925
|
};
|
|
32825
33926
|
}
|
|
32826
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
32827
|
-
|
|
32828
|
-
|
|
33927
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and26(
|
|
33928
|
+
eq31(backlinkSummaries.projectId, ctx.project.id),
|
|
33929
|
+
eq31(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
32829
33930
|
)).limit(1).get() : false;
|
|
32830
33931
|
return {
|
|
32831
33932
|
status: CheckStatuses.ok,
|
|
@@ -32985,7 +34086,7 @@ var BING_AUTH_CHECKS = [
|
|
|
32985
34086
|
];
|
|
32986
34087
|
|
|
32987
34088
|
// ../api-routes/src/doctor/checks/content.ts
|
|
32988
|
-
import { eq as
|
|
34089
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
32989
34090
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
32990
34091
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
32991
34092
|
function skippedNoProject2() {
|
|
@@ -32998,7 +34099,7 @@ function skippedNoProject2() {
|
|
|
32998
34099
|
}
|
|
32999
34100
|
function loadProject(ctx) {
|
|
33000
34101
|
if (!ctx.project) return null;
|
|
33001
|
-
return ctx.db.select().from(projects).where(
|
|
34102
|
+
return ctx.db.select().from(projects).where(eq32(projects.id, ctx.project.id)).get() ?? null;
|
|
33002
34103
|
}
|
|
33003
34104
|
function percent(value) {
|
|
33004
34105
|
return Math.round(value * 100);
|
|
@@ -33090,7 +34191,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
33090
34191
|
);
|
|
33091
34192
|
|
|
33092
34193
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
33093
|
-
import { eq as
|
|
34194
|
+
import { eq as eq33 } from "drizzle-orm";
|
|
33094
34195
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
33095
34196
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
33096
34197
|
var adsConnectionCheck = {
|
|
@@ -33107,7 +34208,7 @@ var adsConnectionCheck = {
|
|
|
33107
34208
|
remediation: null
|
|
33108
34209
|
};
|
|
33109
34210
|
}
|
|
33110
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
34211
|
+
const row = ctx.db.select().from(adsConnections).where(eq33(adsConnections.projectId, ctx.project.id)).get();
|
|
33111
34212
|
if (!row) {
|
|
33112
34213
|
return {
|
|
33113
34214
|
status: CheckStatuses.skipped,
|
|
@@ -33157,7 +34258,7 @@ var adsRecentSyncCheck = {
|
|
|
33157
34258
|
remediation: null
|
|
33158
34259
|
};
|
|
33159
34260
|
}
|
|
33160
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
34261
|
+
const row = ctx.db.select().from(adsConnections).where(eq33(adsConnections.projectId, ctx.project.id)).get();
|
|
33161
34262
|
if (!row) {
|
|
33162
34263
|
return {
|
|
33163
34264
|
status: CheckStatuses.skipped,
|
|
@@ -33348,7 +34449,7 @@ var ga4ConnectionCheck = {
|
|
|
33348
34449
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
33349
34450
|
|
|
33350
34451
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
33351
|
-
import { and as
|
|
34452
|
+
import { and as and27, eq as eq34 } from "drizzle-orm";
|
|
33352
34453
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
33353
34454
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
33354
34455
|
function skippedNoProject3() {
|
|
@@ -33581,7 +34682,7 @@ var recentSyncCheck = {
|
|
|
33581
34682
|
title: "GBP recent sync",
|
|
33582
34683
|
run: (ctx) => {
|
|
33583
34684
|
if (!ctx.project) return skippedNoProject3();
|
|
33584
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
34685
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and27(eq34(gbpLocations.projectId, ctx.project.id), eq34(gbpLocations.selected, true))).all();
|
|
33585
34686
|
if (selected.length === 0) {
|
|
33586
34687
|
return {
|
|
33587
34688
|
status: CheckStatuses.skipped,
|
|
@@ -33641,7 +34742,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
33641
34742
|
);
|
|
33642
34743
|
|
|
33643
34744
|
// ../api-routes/src/doctor/checks/places.ts
|
|
33644
|
-
import { eq as
|
|
34745
|
+
import { eq as eq35 } from "drizzle-orm";
|
|
33645
34746
|
var apiKeyCheck = {
|
|
33646
34747
|
id: "gbp.places.api-key",
|
|
33647
34748
|
category: CheckCategories.auth,
|
|
@@ -33686,7 +34787,7 @@ var apiKeyCheck = {
|
|
|
33686
34787
|
details: { tier: cfg.tier }
|
|
33687
34788
|
};
|
|
33688
34789
|
}
|
|
33689
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
34790
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq35(gbpLocations.projectId, ctx.project.id)).all();
|
|
33690
34791
|
const selected = rows.filter((r) => r.selected);
|
|
33691
34792
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
33692
34793
|
const details = {
|
|
@@ -34183,7 +35284,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
34183
35284
|
];
|
|
34184
35285
|
|
|
34185
35286
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
34186
|
-
import { and as
|
|
35287
|
+
import { and as and28, eq as eq36, gte as gte7, ne as ne4, sql as sql13 } from "drizzle-orm";
|
|
34187
35288
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
34188
35289
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
34189
35290
|
function skippedNoProject5() {
|
|
@@ -34197,8 +35298,8 @@ function skippedNoProject5() {
|
|
|
34197
35298
|
function loadProbes(ctx) {
|
|
34198
35299
|
if (!ctx.project) return [];
|
|
34199
35300
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
34200
|
-
|
|
34201
|
-
|
|
35301
|
+
and28(
|
|
35302
|
+
eq36(trafficSources.projectId, ctx.project.id),
|
|
34202
35303
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
34203
35304
|
)
|
|
34204
35305
|
).all();
|
|
@@ -34278,17 +35379,17 @@ var recentDataCheck = {
|
|
|
34278
35379
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
34279
35380
|
const recentCrawlers = Number(
|
|
34280
35381
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
34281
|
-
|
|
34282
|
-
|
|
34283
|
-
|
|
35382
|
+
and28(
|
|
35383
|
+
eq36(crawlerEventsHourly.projectId, ctx.project.id),
|
|
35384
|
+
gte7(crawlerEventsHourly.tsHour, warnCutoff)
|
|
34284
35385
|
)
|
|
34285
35386
|
).get()?.total ?? 0
|
|
34286
35387
|
);
|
|
34287
35388
|
const recentReferrals = Number(
|
|
34288
35389
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
34289
|
-
|
|
34290
|
-
|
|
34291
|
-
|
|
35390
|
+
and28(
|
|
35391
|
+
eq36(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
35392
|
+
gte7(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
34292
35393
|
)
|
|
34293
35394
|
).get()?.total ?? 0
|
|
34294
35395
|
);
|
|
@@ -34302,17 +35403,17 @@ var recentDataCheck = {
|
|
|
34302
35403
|
}
|
|
34303
35404
|
const olderCrawlers = Number(
|
|
34304
35405
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
34305
|
-
|
|
34306
|
-
|
|
34307
|
-
|
|
35406
|
+
and28(
|
|
35407
|
+
eq36(crawlerEventsHourly.projectId, ctx.project.id),
|
|
35408
|
+
gte7(crawlerEventsHourly.tsHour, failCutoff)
|
|
34308
35409
|
)
|
|
34309
35410
|
).get()?.total ?? 0
|
|
34310
35411
|
);
|
|
34311
35412
|
const olderReferrals = Number(
|
|
34312
35413
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
34313
|
-
|
|
34314
|
-
|
|
34315
|
-
|
|
35414
|
+
and28(
|
|
35415
|
+
eq36(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
35416
|
+
gte7(aiReferralEventsHourly.tsHour, failCutoff)
|
|
34316
35417
|
)
|
|
34317
35418
|
).get()?.total ?? 0
|
|
34318
35419
|
);
|
|
@@ -34727,7 +35828,7 @@ async function doctorRoutes(app, opts) {
|
|
|
34727
35828
|
|
|
34728
35829
|
// ../api-routes/src/discovery/routes.ts
|
|
34729
35830
|
import crypto28 from "crypto";
|
|
34730
|
-
import { and 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";
|
|
34731
35832
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
34732
35833
|
async function discoveryRoutes(app, opts) {
|
|
34733
35834
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -34761,13 +35862,13 @@ async function discoveryRoutes(app, opts) {
|
|
|
34761
35862
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34762
35863
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
34763
35864
|
const decision = app.db.transaction((tx) => {
|
|
34764
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
34765
|
-
|
|
34766
|
-
|
|
35865
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and29(
|
|
35866
|
+
eq37(discoverySessions.projectId, project.id),
|
|
35867
|
+
eq37(discoverySessions.icpDescription, icpDescription),
|
|
34767
35868
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
34768
35869
|
// semantics, so a request with a different (or no) buyer must start
|
|
34769
35870
|
// its own session, never adopt another buyer's probes.
|
|
34770
|
-
parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) :
|
|
35871
|
+
parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) : eq37(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
34771
35872
|
// Locations are identity too: a different service-area subset seeds
|
|
34772
35873
|
// and probes a different geo, so it must never reuse another geo's
|
|
34773
35874
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -34777,17 +35878,17 @@ async function discoveryRoutes(app, opts) {
|
|
|
34777
35878
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
34778
35879
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
34779
35880
|
// upgrade, never a wrong reuse.
|
|
34780
|
-
locations.length === 0 ? or5(isNull(discoverySessions.locations),
|
|
35881
|
+
locations.length === 0 ? or5(isNull(discoverySessions.locations), eq37(discoverySessions.locations, locations)) : eq37(discoverySessions.locations, locations),
|
|
34781
35882
|
// Seed provider set is identity: a different phrasing distribution
|
|
34782
35883
|
// must never reuse another set's session. Null (= Gemini-only
|
|
34783
35884
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
34784
|
-
seedProviders == null ? isNull(discoverySessions.seedProviders) :
|
|
35885
|
+
seedProviders == null ? isNull(discoverySessions.seedProviders) : eq37(discoverySessions.seedProviders, seedProviders),
|
|
34785
35886
|
inArray13(discoverySessions.status, [
|
|
34786
35887
|
DiscoverySessionStatuses.queued,
|
|
34787
35888
|
DiscoverySessionStatuses.seeding,
|
|
34788
35889
|
DiscoverySessionStatuses.probing
|
|
34789
35890
|
]),
|
|
34790
|
-
|
|
35891
|
+
gte8(discoverySessions.createdAt, ageFloorIso)
|
|
34791
35892
|
)).orderBy(desc16(discoverySessions.createdAt)).get();
|
|
34792
35893
|
if (existing && existing.runId) {
|
|
34793
35894
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
@@ -34857,7 +35958,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34857
35958
|
const project = resolveProject(app.db, request.params.name);
|
|
34858
35959
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
34859
35960
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
34860
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
35961
|
+
const rows = app.db.select().from(discoverySessions).where(eq37(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
34861
35962
|
return reply.send(rows.map(serializeSession));
|
|
34862
35963
|
}
|
|
34863
35964
|
);
|
|
@@ -34865,11 +35966,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
34865
35966
|
"/projects/:name/discover/sessions/:id",
|
|
34866
35967
|
async (request, reply) => {
|
|
34867
35968
|
const project = resolveProject(app.db, request.params.name);
|
|
34868
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
35969
|
+
const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
|
|
34869
35970
|
if (!session || session.projectId !== project.id) {
|
|
34870
35971
|
throw notFound("Discovery session", request.params.id);
|
|
34871
35972
|
}
|
|
34872
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
35973
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
|
|
34873
35974
|
const detail = {
|
|
34874
35975
|
...serializeSession(session),
|
|
34875
35976
|
probes: probeRows.map(serializeProbe)
|
|
@@ -34881,7 +35982,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34881
35982
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
34882
35983
|
async (request, reply) => {
|
|
34883
35984
|
const project = resolveProject(app.db, request.params.name);
|
|
34884
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
35985
|
+
const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
|
|
34885
35986
|
if (!session || session.projectId !== project.id) {
|
|
34886
35987
|
throw notFound("Discovery session", request.params.id);
|
|
34887
35988
|
}
|
|
@@ -34889,7 +35990,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34889
35990
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
34890
35991
|
const applyAnchor = request.query.anchor !== "false";
|
|
34891
35992
|
const provider = session.seedProvider ?? "gemini";
|
|
34892
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
35993
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
|
|
34893
35994
|
const extract = opts.harvestSearchQueries;
|
|
34894
35995
|
const probesWithQueries = probeRows.map((row) => {
|
|
34895
35996
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -34900,7 +36001,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34900
36001
|
return { searchQueries: [] };
|
|
34901
36002
|
}
|
|
34902
36003
|
});
|
|
34903
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
36004
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq37(queries.projectId, project.id)).all().map((r) => r.query);
|
|
34904
36005
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
34905
36006
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
34906
36007
|
effectiveDomains(project)
|
|
@@ -34947,12 +36048,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34947
36048
|
"/projects/:name/discover/sessions/:id/promote",
|
|
34948
36049
|
async (request, reply) => {
|
|
34949
36050
|
const project = resolveProject(app.db, request.params.name);
|
|
34950
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
36051
|
+
const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
|
|
34951
36052
|
if (!session || session.projectId !== project.id) {
|
|
34952
36053
|
throw notFound("Discovery session", request.params.id);
|
|
34953
36054
|
}
|
|
34954
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34955
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
36055
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
|
|
36056
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
34956
36057
|
const seenCompetitors = new Set(existingCompetitors);
|
|
34957
36058
|
const cited = /* @__PURE__ */ new Set();
|
|
34958
36059
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -34981,7 +36082,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34981
36082
|
);
|
|
34982
36083
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
34983
36084
|
const project = resolveProject(app.db, request.params.name);
|
|
34984
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
36085
|
+
const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
|
|
34985
36086
|
if (!session || session.projectId !== project.id) {
|
|
34986
36087
|
throw notFound("Discovery session", request.params.id);
|
|
34987
36088
|
}
|
|
@@ -35004,7 +36105,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
35004
36105
|
const bucketSet = new Set(buckets);
|
|
35005
36106
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
35006
36107
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
35007
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
36108
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
|
|
35008
36109
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
35009
36110
|
for (const probe of probeRows) {
|
|
35010
36111
|
if (!probe.bucket) continue;
|
|
@@ -35012,7 +36113,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
35012
36113
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
35013
36114
|
}
|
|
35014
36115
|
const existingQueries = new Set(
|
|
35015
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
36116
|
+
app.db.select({ query: queries.query }).from(queries).where(eq37(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
35016
36117
|
);
|
|
35017
36118
|
const promotedQueries = [];
|
|
35018
36119
|
const skippedQueries = [];
|
|
@@ -35028,7 +36129,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
35028
36129
|
const skippedCompetitors = [];
|
|
35029
36130
|
if (includeCompetitors) {
|
|
35030
36131
|
const existingCompetitors = new Set(
|
|
35031
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
36132
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
35032
36133
|
);
|
|
35033
36134
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
35034
36135
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -35147,7 +36248,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
35147
36248
|
|
|
35148
36249
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
35149
36250
|
import crypto29 from "crypto";
|
|
35150
|
-
import { eq as
|
|
36251
|
+
import { eq as eq38 } from "drizzle-orm";
|
|
35151
36252
|
var DEFAULT_MAX_PROBES = 100;
|
|
35152
36253
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
35153
36254
|
function classifyProbeBucket(input) {
|
|
@@ -35234,7 +36335,7 @@ async function executeDiscovery(opts) {
|
|
|
35234
36335
|
status: DiscoverySessionStatuses.seeding,
|
|
35235
36336
|
dedupThreshold,
|
|
35236
36337
|
startedAt
|
|
35237
|
-
}).where(
|
|
36338
|
+
}).where(eq38(discoverySessions.id, opts.sessionId)).run();
|
|
35238
36339
|
const seedResult = await opts.deps.seed({
|
|
35239
36340
|
project: opts.project,
|
|
35240
36341
|
icpDescription: opts.icpDescription,
|
|
@@ -35295,7 +36396,7 @@ async function executeDiscovery(opts) {
|
|
|
35295
36396
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
35296
36397
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
35297
36398
|
warning
|
|
35298
|
-
}).where(
|
|
36399
|
+
}).where(eq38(discoverySessions.id, opts.sessionId)).run();
|
|
35299
36400
|
const probeLocation = opts.locations?.[0];
|
|
35300
36401
|
const probeResults = await mapWithConcurrency(
|
|
35301
36402
|
probedCanonicals,
|
|
@@ -35349,7 +36450,7 @@ async function executeDiscovery(opts) {
|
|
|
35349
36450
|
wastedCount: buckets["wasted-surface"],
|
|
35350
36451
|
competitorMap,
|
|
35351
36452
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35352
|
-
}).where(
|
|
36453
|
+
}).where(eq38(discoverySessions.id, opts.sessionId)).run();
|
|
35353
36454
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
35354
36455
|
return {
|
|
35355
36456
|
buckets,
|
|
@@ -35389,7 +36490,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
35389
36490
|
status: DiscoverySessionStatuses.failed,
|
|
35390
36491
|
error,
|
|
35391
36492
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35392
|
-
}).where(
|
|
36493
|
+
}).where(eq38(discoverySessions.id, sessionId)).run();
|
|
35393
36494
|
}
|
|
35394
36495
|
function dedupeStrings(input) {
|
|
35395
36496
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -35407,7 +36508,7 @@ function dedupeStrings(input) {
|
|
|
35407
36508
|
|
|
35408
36509
|
// ../api-routes/src/technical-aeo.ts
|
|
35409
36510
|
import crypto30 from "crypto";
|
|
35410
|
-
import { and 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";
|
|
35411
36512
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
35412
36513
|
function emptyScore(projectName) {
|
|
35413
36514
|
return {
|
|
@@ -35439,16 +36540,20 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
35439
36540
|
async function technicalAeoRoutes(app, opts) {
|
|
35440
36541
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
35441
36542
|
const project = resolveProject(app.db, request.params.name);
|
|
35442
|
-
const
|
|
35443
|
-
|
|
35444
|
-
|
|
36543
|
+
const baseFilters = [
|
|
36544
|
+
eq39(siteAuditSnapshots.projectId, project.id),
|
|
36545
|
+
eq39(runs.kind, RunKinds["site-audit"]),
|
|
35445
36546
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35446
36547
|
notProbeRun()
|
|
35447
|
-
|
|
35448
|
-
const
|
|
35449
|
-
|
|
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
|
+
}
|
|
35450
36555
|
const snap = latest.snap;
|
|
35451
|
-
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;
|
|
35452
36557
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
35453
36558
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
35454
36559
|
return {
|
|
@@ -35474,24 +36579,29 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35474
36579
|
});
|
|
35475
36580
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
35476
36581
|
const project = resolveProject(app.db, request.params.name);
|
|
35477
|
-
const
|
|
35478
|
-
|
|
35479
|
-
|
|
36582
|
+
const targetFilters = [
|
|
36583
|
+
eq39(siteAuditSnapshots.projectId, project.id),
|
|
36584
|
+
eq39(runs.kind, RunKinds["site-audit"]),
|
|
35480
36585
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35481
36586
|
notProbeRun()
|
|
35482
|
-
|
|
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
|
+
}
|
|
35483
36593
|
if (!latest) {
|
|
35484
36594
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
35485
36595
|
}
|
|
35486
36596
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
35487
|
-
const conds = [
|
|
35488
|
-
if (statusFilter) conds.push(
|
|
35489
|
-
const where =
|
|
36597
|
+
const conds = [eq39(siteAuditPages.runId, latest.runId)];
|
|
36598
|
+
if (statusFilter) conds.push(eq39(siteAuditPages.status, statusFilter));
|
|
36599
|
+
const where = and30(...conds);
|
|
35490
36600
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
35491
36601
|
const total = totalRow?.value ?? 0;
|
|
35492
36602
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
35493
36603
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
35494
|
-
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);
|
|
35495
36605
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
35496
36606
|
const pages = rows.map((row) => ({
|
|
35497
36607
|
url: row.url,
|
|
@@ -35510,9 +36620,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35510
36620
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
35511
36621
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
35512
36622
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
35513
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
35514
|
-
|
|
35515
|
-
|
|
36623
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(
|
|
36624
|
+
eq39(siteAuditSnapshots.projectId, project.id),
|
|
36625
|
+
eq39(runs.kind, RunKinds["site-audit"]),
|
|
35516
36626
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35517
36627
|
notProbeRun()
|
|
35518
36628
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -35524,9 +36634,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35524
36634
|
if (!parsed.success) {
|
|
35525
36635
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
35526
36636
|
}
|
|
35527
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
35528
|
-
|
|
35529
|
-
|
|
36637
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and30(
|
|
36638
|
+
eq39(runs.projectId, project.id),
|
|
36639
|
+
eq39(runs.kind, RunKinds["site-audit"]),
|
|
35530
36640
|
inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
35531
36641
|
)).get();
|
|
35532
36642
|
if (existing) {
|
|
@@ -35632,6 +36742,7 @@ async function apiRoutes(app, opts) {
|
|
|
35632
36742
|
await api.register(reportRoutes);
|
|
35633
36743
|
await api.register(citationRoutes);
|
|
35634
36744
|
await api.register(visibilityStatsRoutes);
|
|
36745
|
+
await api.register(resultsExportRoutes);
|
|
35635
36746
|
await api.register(compositeRoutes);
|
|
35636
36747
|
await api.register(contentRoutes, {
|
|
35637
36748
|
explainContentRecommendation: opts.explainContentRecommendation,
|
|
@@ -35665,7 +36776,8 @@ async function apiRoutes(app, opts) {
|
|
|
35665
36776
|
await api.register(adsRoutes, {
|
|
35666
36777
|
adsCredentialStore: opts.adsCredentialStore,
|
|
35667
36778
|
verifyAdsAccount: opts.verifyAdsAccount,
|
|
35668
|
-
onAdsSyncRequested: opts.onAdsSyncRequested
|
|
36779
|
+
onAdsSyncRequested: opts.onAdsSyncRequested,
|
|
36780
|
+
adsOperator: opts.adsOperator
|
|
35669
36781
|
});
|
|
35670
36782
|
await api.register(bingRoutes, {
|
|
35671
36783
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -36118,9 +37230,9 @@ var IntelligenceService = class {
|
|
|
36118
37230
|
*/
|
|
36119
37231
|
analyzeAndPersist(runId, projectId) {
|
|
36120
37232
|
const recentRuns = this.db.select().from(runs).where(
|
|
36121
|
-
|
|
36122
|
-
|
|
36123
|
-
or6(
|
|
37233
|
+
and31(
|
|
37234
|
+
eq40(runs.projectId, projectId),
|
|
37235
|
+
or6(eq40(runs.status, "completed"), eq40(runs.status, "partial")),
|
|
36124
37236
|
// Defensive: RunCoordinator already skips probes before this is
|
|
36125
37237
|
// called, but if a future call site invokes analyzeAndPersist
|
|
36126
37238
|
// directly for a probe, probes still must not pollute the
|
|
@@ -36202,7 +37314,7 @@ var IntelligenceService = class {
|
|
|
36202
37314
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
36203
37315
|
*/
|
|
36204
37316
|
analyzeAndPersistGbp(runId, projectId) {
|
|
36205
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
37317
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq40(runs.id, runId)).get();
|
|
36206
37318
|
if (!runRow) {
|
|
36207
37319
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
36208
37320
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -36210,11 +37322,11 @@ var IntelligenceService = class {
|
|
|
36210
37322
|
}
|
|
36211
37323
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
36212
37324
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
36213
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
36214
|
-
|
|
36215
|
-
|
|
36216
|
-
|
|
36217
|
-
|
|
37325
|
+
const selected = this.db.select().from(gbpLocations).where(and31(
|
|
37326
|
+
eq40(gbpLocations.projectId, projectId),
|
|
37327
|
+
eq40(gbpLocations.selected, true),
|
|
37328
|
+
gte9(gbpLocations.syncedAt, windowStart),
|
|
37329
|
+
lte5(gbpLocations.syncedAt, windowEnd)
|
|
36218
37330
|
)).all();
|
|
36219
37331
|
if (selected.length === 0) {
|
|
36220
37332
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -36247,12 +37359,12 @@ var IntelligenceService = class {
|
|
|
36247
37359
|
}
|
|
36248
37360
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
36249
37361
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
36250
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
36251
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
36252
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
36253
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(
|
|
37362
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and31(eq40(gbpDailyMetrics.projectId, projectId), eq40(gbpDailyMetrics.locationName, locationName))).all();
|
|
37363
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and31(eq40(gbpPlaceActions.projectId, projectId), eq40(gbpPlaceActions.locationName, locationName))).all();
|
|
37364
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and31(eq40(gbpLodgingSnapshots.projectId, projectId), eq40(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
37365
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and31(eq40(gbpLocations.projectId, projectId), eq40(gbpLocations.locationName, locationName))).get();
|
|
36254
37366
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
36255
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
37367
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and31(eq40(gbpPlaceDetails.projectId, projectId), eq40(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
36256
37368
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
36257
37369
|
const summary = buildGbpSummary({
|
|
36258
37370
|
locationName,
|
|
@@ -36288,7 +37400,7 @@ var IntelligenceService = class {
|
|
|
36288
37400
|
/** Build the month-over-month keyword series for a location from the
|
|
36289
37401
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
36290
37402
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
36291
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
37403
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and31(eq40(gbpKeywordMonthly.projectId, projectId), eq40(gbpKeywordMonthly.locationName, locationName))).all();
|
|
36292
37404
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
36293
37405
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
36294
37406
|
const recentMonth = months[0] ?? null;
|
|
@@ -36319,7 +37431,7 @@ var IntelligenceService = class {
|
|
|
36319
37431
|
*/
|
|
36320
37432
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
36321
37433
|
const covered = new Set(coveredLocationNames);
|
|
36322
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
37434
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and31(eq40(insights.projectId, projectId), eq40(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
36323
37435
|
const staleIds = [];
|
|
36324
37436
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
36325
37437
|
for (const row of existing) {
|
|
@@ -36330,7 +37442,7 @@ var IntelligenceService = class {
|
|
|
36330
37442
|
}
|
|
36331
37443
|
this.db.transaction((tx) => {
|
|
36332
37444
|
for (const id of staleIds) {
|
|
36333
|
-
tx.delete(insights).where(
|
|
37445
|
+
tx.delete(insights).where(eq40(insights.id, id)).run();
|
|
36334
37446
|
}
|
|
36335
37447
|
for (const insight of gbpInsights) {
|
|
36336
37448
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -36408,7 +37520,7 @@ var IntelligenceService = class {
|
|
|
36408
37520
|
* create per run + aggregate). DB is left untouched.
|
|
36409
37521
|
*/
|
|
36410
37522
|
backfill(projectName, opts, onProgress) {
|
|
36411
|
-
const project = this.db.select().from(projects).where(
|
|
37523
|
+
const project = this.db.select().from(projects).where(eq40(projects.name, projectName)).get();
|
|
36412
37524
|
if (!project) {
|
|
36413
37525
|
throw new Error(`Project "${projectName}" not found`);
|
|
36414
37526
|
}
|
|
@@ -36421,13 +37533,13 @@ var IntelligenceService = class {
|
|
|
36421
37533
|
sinceTimestamp = parsed;
|
|
36422
37534
|
}
|
|
36423
37535
|
const allRuns = this.db.select().from(runs).where(
|
|
36424
|
-
|
|
36425
|
-
|
|
36426
|
-
or6(
|
|
37536
|
+
and31(
|
|
37537
|
+
eq40(runs.projectId, project.id),
|
|
37538
|
+
or6(eq40(runs.status, "completed"), eq40(runs.status, "partial")),
|
|
36427
37539
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
36428
37540
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36429
37541
|
)
|
|
36430
|
-
).orderBy(
|
|
37542
|
+
).orderBy(asc8(runs.finishedAt)).all();
|
|
36431
37543
|
let startIdx = 0;
|
|
36432
37544
|
let endIdx = allRuns.length;
|
|
36433
37545
|
if (opts?.fromRunId) {
|
|
@@ -36502,7 +37614,7 @@ var IntelligenceService = class {
|
|
|
36502
37614
|
return { processed, skipped, totalInsights };
|
|
36503
37615
|
}
|
|
36504
37616
|
loadTrackedCompetitors(projectId) {
|
|
36505
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
37617
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq40(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
36506
37618
|
}
|
|
36507
37619
|
/**
|
|
36508
37620
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -36523,15 +37635,15 @@ var IntelligenceService = class {
|
|
|
36523
37635
|
}
|
|
36524
37636
|
persistResult(result, runId, projectId) {
|
|
36525
37637
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
36526
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
37638
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq40(insights.runId, runId)).all();
|
|
36527
37639
|
for (const row of existingInsights) {
|
|
36528
37640
|
if (row.dismissed) {
|
|
36529
37641
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
36530
37642
|
}
|
|
36531
37643
|
}
|
|
36532
37644
|
this.db.transaction((tx) => {
|
|
36533
|
-
tx.delete(insights).where(
|
|
36534
|
-
tx.delete(healthSnapshots).where(
|
|
37645
|
+
tx.delete(insights).where(eq40(insights.runId, runId)).run();
|
|
37646
|
+
tx.delete(healthSnapshots).where(eq40(healthSnapshots.runId, runId)).run();
|
|
36535
37647
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36536
37648
|
for (const insight of result.insights) {
|
|
36537
37649
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -36584,24 +37696,24 @@ var IntelligenceService = class {
|
|
|
36584
37696
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
36585
37697
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
36586
37698
|
if (regressions.length === 0) return rawInsights;
|
|
36587
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
37699
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq40(gscSearchData.projectId, projectId)).all();
|
|
36588
37700
|
const gscConnected = gscRows.length > 0;
|
|
36589
37701
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
36590
37702
|
for (const row of gscRows) {
|
|
36591
37703
|
const key = row.query.toLowerCase();
|
|
36592
37704
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
36593
37705
|
}
|
|
36594
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
37706
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq40(projects.id, projectId)).get();
|
|
36595
37707
|
const locationCount = Math.max(
|
|
36596
37708
|
1,
|
|
36597
37709
|
(projectRow?.locations ?? []).length
|
|
36598
37710
|
);
|
|
36599
37711
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
36600
37712
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
36601
|
-
|
|
36602
|
-
|
|
36603
|
-
|
|
36604
|
-
or6(
|
|
37713
|
+
and31(
|
|
37714
|
+
eq40(runs.projectId, projectId),
|
|
37715
|
+
eq40(runs.kind, RunKinds["answer-visibility"]),
|
|
37716
|
+
or6(eq40(runs.status, "completed"), eq40(runs.status, "partial")),
|
|
36605
37717
|
// Defensive — see top of file.
|
|
36606
37718
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36607
37719
|
)
|
|
@@ -36621,7 +37733,7 @@ var IntelligenceService = class {
|
|
|
36621
37733
|
const haveHistory = recentRunIds.length > 0;
|
|
36622
37734
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
36623
37735
|
if (haveHistory) {
|
|
36624
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
37736
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and31(eq40(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
|
|
36625
37737
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
36626
37738
|
for (const row of priorRows) {
|
|
36627
37739
|
if (!row.runId) continue;
|
|
@@ -36650,7 +37762,7 @@ var IntelligenceService = class {
|
|
|
36650
37762
|
});
|
|
36651
37763
|
}
|
|
36652
37764
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
36653
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
37765
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq40(projects.id, projectId)).get();
|
|
36654
37766
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
36655
37767
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
36656
37768
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -36667,7 +37779,7 @@ var IntelligenceService = class {
|
|
|
36667
37779
|
citedDomains: querySnapshots.citedDomains,
|
|
36668
37780
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
36669
37781
|
snapshotLocation: querySnapshots.location
|
|
36670
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
37782
|
+
}).from(querySnapshots).leftJoin(queries, eq40(querySnapshots.queryId, queries.id)).where(eq40(querySnapshots.runId, runId)).all();
|
|
36671
37783
|
const snapshots = [];
|
|
36672
37784
|
let orphanCount = 0;
|
|
36673
37785
|
for (const r of rows) {
|