@canonry/canonry 4.117.0 → 4.117.1
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/dist/{chunk-PJATAPZW.js → chunk-BMBHUFMI.js} +13 -0
- package/dist/{chunk-ZZEA774R.js → chunk-NY7SC4ES.js} +5 -4
- package/dist/{chunk-FVVKNMZW.js → chunk-V4SW7SVX.js} +734 -652
- package/dist/cli.js +3 -3
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-J7G4CMI5.js → intelligence-service-C5YLASIP.js} +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +7 -7
|
@@ -283,7 +283,7 @@ import {
|
|
|
283
283
|
} from "./chunk-IAJ5TQ4S.js";
|
|
284
284
|
|
|
285
285
|
// src/intelligence-service.ts
|
|
286
|
-
import { eq as
|
|
286
|
+
import { eq as eq39, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray15, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
287
287
|
|
|
288
288
|
// ../db/src/client.ts
|
|
289
289
|
import { mkdirSync } from "fs";
|
|
@@ -1668,6 +1668,32 @@ CREATE TABLE IF NOT EXISTS _migrations (
|
|
|
1668
1668
|
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
1669
1669
|
);
|
|
1670
1670
|
`;
|
|
1671
|
+
function relinkOrphanedSnapshotQueryIds(tx) {
|
|
1672
|
+
const orphans = tx.all(sql`
|
|
1673
|
+
SELECT qs.id AS snapId, qs.query_text AS text, r.project_id AS projectId
|
|
1674
|
+
FROM query_snapshots qs
|
|
1675
|
+
JOIN runs r ON r.id = qs.run_id
|
|
1676
|
+
WHERE qs.query_id IS NULL AND qs.query_text IS NOT NULL
|
|
1677
|
+
`);
|
|
1678
|
+
if (orphans.length === 0) return;
|
|
1679
|
+
const queryRows = tx.all(sql`SELECT id, project_id AS projectId, query FROM queries`);
|
|
1680
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
1681
|
+
for (const row of queryRows) {
|
|
1682
|
+
let perProject = byProject.get(row.projectId);
|
|
1683
|
+
if (!perProject) {
|
|
1684
|
+
perProject = /* @__PURE__ */ new Map();
|
|
1685
|
+
byProject.set(row.projectId, perProject);
|
|
1686
|
+
}
|
|
1687
|
+
const key = normalizeQueryText(row.query);
|
|
1688
|
+
if (!perProject.has(key)) perProject.set(key, row.id);
|
|
1689
|
+
}
|
|
1690
|
+
for (const orphan of orphans) {
|
|
1691
|
+
const queryId = byProject.get(orphan.projectId)?.get(normalizeQueryText(orphan.text));
|
|
1692
|
+
if (queryId) {
|
|
1693
|
+
tx.run(sql`UPDATE query_snapshots SET query_id = ${queryId} WHERE id = ${orphan.snapId}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1671
1697
|
var MIGRATION_VERSIONS = [
|
|
1672
1698
|
{
|
|
1673
1699
|
version: 2,
|
|
@@ -3558,6 +3584,19 @@ var MIGRATION_VERSIONS = [
|
|
|
3558
3584
|
`ALTER TABLE ai_referral_events_hourly ADD COLUMN paid_sessions_or_hits INTEGER NOT NULL DEFAULT 0`,
|
|
3559
3585
|
`ALTER TABLE ai_referral_events_hourly ADD COLUMN organic_sessions_or_hits INTEGER NOT NULL DEFAULT 0`
|
|
3560
3586
|
]
|
|
3587
|
+
},
|
|
3588
|
+
{
|
|
3589
|
+
// One-time self-heal for installs whose snapshot->query FKs were orphaned
|
|
3590
|
+
// by the pre-fix delete-all + reinsert replace paths. Data-only UPDATEs
|
|
3591
|
+
// (no schema change) via run() — TS matching because SQLite lower() is
|
|
3592
|
+
// ASCII-only; see the doc comment on relinkOrphanedSnapshotQueryIds and
|
|
3593
|
+
// the downgrade-safety RUN_HOOK_ALLOWLIST justification.
|
|
3594
|
+
version: 98,
|
|
3595
|
+
name: "relink-orphaned-snapshot-query-ids",
|
|
3596
|
+
statements: [],
|
|
3597
|
+
run: (tx) => {
|
|
3598
|
+
relinkOrphanedSnapshotQueryIds(tx);
|
|
3599
|
+
}
|
|
3561
3600
|
}
|
|
3562
3601
|
];
|
|
3563
3602
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -6465,18 +6504,88 @@ function aliasArraysEqual(a, b) {
|
|
|
6465
6504
|
}
|
|
6466
6505
|
|
|
6467
6506
|
// ../api-routes/src/queries.ts
|
|
6507
|
+
import crypto6 from "crypto";
|
|
6508
|
+
import { and, eq as eq5, inArray as inArray2, sql as sql4 } from "drizzle-orm";
|
|
6509
|
+
|
|
6510
|
+
// ../api-routes/src/query-replace.ts
|
|
6468
6511
|
import crypto5 from "crypto";
|
|
6469
|
-
import {
|
|
6512
|
+
import { eq as eq4, inArray } from "drizzle-orm";
|
|
6470
6513
|
function preserveSnapshotQueryText(tx, projectId, queryIds) {
|
|
6471
6514
|
const candidates = queryIds && queryIds.length > 0 ? tx.select({ id: queries.id, text: queries.query }).from(queries).where(inArray(queries.id, queryIds)).all() : tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
|
|
6472
6515
|
for (const q of candidates) {
|
|
6473
6516
|
tx.update(querySnapshots).set({ queryText: q.text }).where(eq4(querySnapshots.queryId, q.id)).run();
|
|
6474
6517
|
}
|
|
6475
6518
|
}
|
|
6519
|
+
function diffProjectQueries(existing, incomingTexts) {
|
|
6520
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6521
|
+
for (const row of existing) {
|
|
6522
|
+
const key = normalizeQueryText(row.text);
|
|
6523
|
+
const group = groups.get(key);
|
|
6524
|
+
if (group) group.push(row);
|
|
6525
|
+
else groups.set(key, [row]);
|
|
6526
|
+
}
|
|
6527
|
+
const kept = [];
|
|
6528
|
+
const duplicates = [];
|
|
6529
|
+
const removed = [];
|
|
6530
|
+
const insertedTexts = [];
|
|
6531
|
+
const seenIncoming = /* @__PURE__ */ new Set();
|
|
6532
|
+
for (const text2 of incomingTexts) {
|
|
6533
|
+
const key = normalizeQueryText(text2);
|
|
6534
|
+
if (seenIncoming.has(key)) continue;
|
|
6535
|
+
seenIncoming.add(key);
|
|
6536
|
+
const group = groups.get(key);
|
|
6537
|
+
if (group) {
|
|
6538
|
+
groups.delete(key);
|
|
6539
|
+
const [canonical, ...extras] = group;
|
|
6540
|
+
kept.push({ id: canonical.id, currentText: canonical.text, incomingText: text2 });
|
|
6541
|
+
for (const extra of extras) {
|
|
6542
|
+
duplicates.push({ id: extra.id, text: extra.text, keptId: canonical.id });
|
|
6543
|
+
}
|
|
6544
|
+
} else {
|
|
6545
|
+
insertedTexts.push(text2);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
for (const group of groups.values()) {
|
|
6549
|
+
for (const row of group) removed.push({ id: row.id, text: row.text });
|
|
6550
|
+
}
|
|
6551
|
+
return { kept, duplicates, removed, insertedTexts };
|
|
6552
|
+
}
|
|
6553
|
+
function replaceProjectQueries(tx, projectId, incomingTexts, now) {
|
|
6554
|
+
const existing = tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
|
|
6555
|
+
const diff = diffProjectQueries(existing, incomingTexts);
|
|
6556
|
+
if (diff.duplicates.length > 0) {
|
|
6557
|
+
for (const dup of diff.duplicates) {
|
|
6558
|
+
tx.update(querySnapshots).set({ queryId: dup.keptId }).where(eq4(querySnapshots.queryId, dup.id)).run();
|
|
6559
|
+
}
|
|
6560
|
+
tx.delete(queries).where(inArray(queries.id, diff.duplicates.map((d) => d.id))).run();
|
|
6561
|
+
}
|
|
6562
|
+
if (diff.removed.length > 0) {
|
|
6563
|
+
const removedIds = diff.removed.map((r) => r.id);
|
|
6564
|
+
preserveSnapshotQueryText(tx, projectId, removedIds);
|
|
6565
|
+
tx.delete(queries).where(inArray(queries.id, removedIds)).run();
|
|
6566
|
+
}
|
|
6567
|
+
for (const keptRow of diff.kept) {
|
|
6568
|
+
if (keptRow.currentText !== keptRow.incomingText) {
|
|
6569
|
+
tx.update(queries).set({ query: keptRow.incomingText }).where(eq4(queries.id, keptRow.id)).run();
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
for (const text2 of diff.insertedTexts) {
|
|
6573
|
+
tx.insert(queries).values({
|
|
6574
|
+
id: crypto5.randomUUID(),
|
|
6575
|
+
projectId,
|
|
6576
|
+
query: text2,
|
|
6577
|
+
provenance: "cli",
|
|
6578
|
+
createdAt: now
|
|
6579
|
+
}).run();
|
|
6580
|
+
}
|
|
6581
|
+
return diff;
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6584
|
+
// ../api-routes/src/queries.ts
|
|
6476
6585
|
async function queryRoutes(app, opts) {
|
|
6477
6586
|
app.get("/projects/:name/queries", async (request, reply) => {
|
|
6478
6587
|
const project = resolveProject(app.db, request.params.name);
|
|
6479
|
-
const rows = app.db.select().from(queries).where(
|
|
6588
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6480
6589
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6481
6590
|
});
|
|
6482
6591
|
app.put("/projects/:name/queries", async (request, reply) => {
|
|
@@ -6487,17 +6596,7 @@ async function queryRoutes(app, opts) {
|
|
|
6487
6596
|
}
|
|
6488
6597
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6489
6598
|
app.db.transaction((tx) => {
|
|
6490
|
-
|
|
6491
|
-
tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
|
|
6492
|
-
for (const q of body.queries) {
|
|
6493
|
-
tx.insert(queries).values({
|
|
6494
|
-
id: crypto5.randomUUID(),
|
|
6495
|
-
projectId: project.id,
|
|
6496
|
-
query: q,
|
|
6497
|
-
provenance: "cli",
|
|
6498
|
-
createdAt: now
|
|
6499
|
-
}).run();
|
|
6500
|
-
}
|
|
6599
|
+
replaceProjectQueries(tx, project.id, body.queries, now);
|
|
6501
6600
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6502
6601
|
projectId: project.id,
|
|
6503
6602
|
actor: "api",
|
|
@@ -6506,7 +6605,7 @@ async function queryRoutes(app, opts) {
|
|
|
6506
6605
|
diff: { queries: body.queries }
|
|
6507
6606
|
}));
|
|
6508
6607
|
});
|
|
6509
|
-
const rows = app.db.select().from(queries).where(
|
|
6608
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6510
6609
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6511
6610
|
});
|
|
6512
6611
|
app.post("/projects/:name/queries/replace-preview", async (request, reply) => {
|
|
@@ -6515,20 +6614,22 @@ async function queryRoutes(app, opts) {
|
|
|
6515
6614
|
if (!body || !Array.isArray(body.queries)) {
|
|
6516
6615
|
throw validationError('Body must contain a "queries" array');
|
|
6517
6616
|
}
|
|
6518
|
-
const currentRows = app.db.select().from(queries).where(
|
|
6617
|
+
const currentRows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6519
6618
|
const currentTexts = currentRows.map((r) => r.query);
|
|
6520
|
-
const
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
const
|
|
6525
|
-
const
|
|
6619
|
+
const diff = diffProjectQueries(
|
|
6620
|
+
currentRows.map((r) => ({ id: r.id, text: r.query })),
|
|
6621
|
+
body.queries
|
|
6622
|
+
);
|
|
6623
|
+
const removed = diff.removed.map((r) => r.text);
|
|
6624
|
+
const added = diff.insertedTexts;
|
|
6625
|
+
const unchanged = diff.kept.map((k) => k.currentText);
|
|
6626
|
+
const removedIds = diff.removed.map((r) => r.id);
|
|
6526
6627
|
let snapshotsDetached = 0;
|
|
6527
6628
|
let affectedQueries = 0;
|
|
6528
|
-
if (
|
|
6529
|
-
const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).where(
|
|
6629
|
+
if (removedIds.length > 0) {
|
|
6630
|
+
const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
|
|
6530
6631
|
snapshotsDetached = snapshotCount?.n ?? 0;
|
|
6531
|
-
const distinctAffected = app.db.select({ n: sql4`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(
|
|
6632
|
+
const distinctAffected = app.db.select({ n: sql4`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
|
|
6532
6633
|
affectedQueries = distinctAffected?.n ?? 0;
|
|
6533
6634
|
}
|
|
6534
6635
|
return reply.send({
|
|
@@ -6545,14 +6646,14 @@ async function queryRoutes(app, opts) {
|
|
|
6545
6646
|
if (!body || !Array.isArray(body.queries) || body.queries.length === 0) {
|
|
6546
6647
|
throw validationError('Body must contain a non-empty "queries" array');
|
|
6547
6648
|
}
|
|
6548
|
-
const existing = app.db.select().from(queries).where(
|
|
6649
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6549
6650
|
const toDelete = new Set(body.queries);
|
|
6550
6651
|
const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
|
|
6551
6652
|
if (idsToDelete.length > 0) {
|
|
6552
6653
|
app.db.transaction((tx) => {
|
|
6553
6654
|
preserveSnapshotQueryText(tx, project.id, idsToDelete);
|
|
6554
6655
|
for (const id of idsToDelete) {
|
|
6555
|
-
tx.delete(queries).where(
|
|
6656
|
+
tx.delete(queries).where(eq5(queries.id, id)).run();
|
|
6556
6657
|
}
|
|
6557
6658
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6558
6659
|
projectId: project.id,
|
|
@@ -6563,18 +6664,18 @@ async function queryRoutes(app, opts) {
|
|
|
6563
6664
|
}));
|
|
6564
6665
|
});
|
|
6565
6666
|
}
|
|
6566
|
-
const rows = app.db.select().from(queries).where(
|
|
6667
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6567
6668
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6568
6669
|
});
|
|
6569
6670
|
app.delete("/projects/:name/queries/:id", async (request, reply) => {
|
|
6570
6671
|
const project = resolveProject(app.db, request.params.name);
|
|
6571
|
-
const query = app.db.select().from(queries).where(and(
|
|
6672
|
+
const query = app.db.select().from(queries).where(and(eq5(queries.projectId, project.id), eq5(queries.id, request.params.id))).get();
|
|
6572
6673
|
if (!query) {
|
|
6573
6674
|
throw notFound("Query", request.params.id);
|
|
6574
6675
|
}
|
|
6575
6676
|
app.db.transaction((tx) => {
|
|
6576
6677
|
preserveSnapshotQueryText(tx, project.id, [query.id]);
|
|
6577
|
-
tx.delete(queries).where(
|
|
6678
|
+
tx.delete(queries).where(eq5(queries.id, query.id)).run();
|
|
6578
6679
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6579
6680
|
projectId: project.id,
|
|
6580
6681
|
actor: "api",
|
|
@@ -6593,13 +6694,13 @@ async function queryRoutes(app, opts) {
|
|
|
6593
6694
|
throw validationError('Body must contain a "queries" array');
|
|
6594
6695
|
}
|
|
6595
6696
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6596
|
-
const existing = app.db.select().from(queries).where(
|
|
6697
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6597
6698
|
const existingSet = new Set(existing.map((q) => q.query));
|
|
6598
6699
|
const added = [];
|
|
6599
6700
|
for (const q of body.queries) {
|
|
6600
6701
|
if (!existingSet.has(q)) {
|
|
6601
6702
|
app.db.insert(queries).values({
|
|
6602
|
-
id:
|
|
6703
|
+
id: crypto6.randomUUID(),
|
|
6603
6704
|
projectId: project.id,
|
|
6604
6705
|
query: q,
|
|
6605
6706
|
provenance: "cli",
|
|
@@ -6618,7 +6719,7 @@ async function queryRoutes(app, opts) {
|
|
|
6618
6719
|
diff: { added }
|
|
6619
6720
|
}));
|
|
6620
6721
|
}
|
|
6621
|
-
const rows = app.db.select().from(queries).where(
|
|
6722
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6622
6723
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6623
6724
|
});
|
|
6624
6725
|
app.post("/projects/:name/queries/generate", async (request, reply) => {
|
|
@@ -6645,7 +6746,7 @@ async function queryRoutes(app, opts) {
|
|
|
6645
6746
|
if (!opts.onGenerateQueries) {
|
|
6646
6747
|
throw notImplemented("Query generation is not supported in this deployment");
|
|
6647
6748
|
}
|
|
6648
|
-
const existingRows = app.db.select().from(queries).where(
|
|
6749
|
+
const existingRows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6649
6750
|
const existingQueries = existingRows.map((r) => r.query);
|
|
6650
6751
|
try {
|
|
6651
6752
|
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
@@ -6663,7 +6764,7 @@ async function queryRoutes(app, opts) {
|
|
|
6663
6764
|
});
|
|
6664
6765
|
app.get("/projects/:name/keywords", async (request, reply) => {
|
|
6665
6766
|
const project = resolveProject(app.db, request.params.name);
|
|
6666
|
-
const rows = app.db.select().from(queries).where(
|
|
6767
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6667
6768
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6668
6769
|
});
|
|
6669
6770
|
app.put("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -6674,17 +6775,7 @@ async function queryRoutes(app, opts) {
|
|
|
6674
6775
|
}
|
|
6675
6776
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6676
6777
|
app.db.transaction((tx) => {
|
|
6677
|
-
|
|
6678
|
-
tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
|
|
6679
|
-
for (const keyword of body.keywords) {
|
|
6680
|
-
tx.insert(queries).values({
|
|
6681
|
-
id: crypto5.randomUUID(),
|
|
6682
|
-
projectId: project.id,
|
|
6683
|
-
query: keyword,
|
|
6684
|
-
provenance: "cli",
|
|
6685
|
-
createdAt: now
|
|
6686
|
-
}).run();
|
|
6687
|
-
}
|
|
6778
|
+
replaceProjectQueries(tx, project.id, body.keywords, now);
|
|
6688
6779
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6689
6780
|
projectId: project.id,
|
|
6690
6781
|
actor: "api",
|
|
@@ -6693,7 +6784,7 @@ async function queryRoutes(app, opts) {
|
|
|
6693
6784
|
diff: { queries: body.keywords }
|
|
6694
6785
|
}));
|
|
6695
6786
|
});
|
|
6696
|
-
const rows = app.db.select().from(queries).where(
|
|
6787
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6697
6788
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6698
6789
|
});
|
|
6699
6790
|
app.delete("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -6702,14 +6793,14 @@ async function queryRoutes(app, opts) {
|
|
|
6702
6793
|
if (!body || !Array.isArray(body.keywords) || body.keywords.length === 0) {
|
|
6703
6794
|
throw validationError('Body must contain a non-empty "keywords" array');
|
|
6704
6795
|
}
|
|
6705
|
-
const existing = app.db.select().from(queries).where(
|
|
6796
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6706
6797
|
const toDelete = new Set(body.keywords);
|
|
6707
6798
|
const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
|
|
6708
6799
|
if (idsToDelete.length > 0) {
|
|
6709
6800
|
app.db.transaction((tx) => {
|
|
6710
6801
|
preserveSnapshotQueryText(tx, project.id, idsToDelete);
|
|
6711
6802
|
for (const id of idsToDelete) {
|
|
6712
|
-
tx.delete(queries).where(
|
|
6803
|
+
tx.delete(queries).where(eq5(queries.id, id)).run();
|
|
6713
6804
|
}
|
|
6714
6805
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6715
6806
|
projectId: project.id,
|
|
@@ -6720,7 +6811,7 @@ async function queryRoutes(app, opts) {
|
|
|
6720
6811
|
}));
|
|
6721
6812
|
});
|
|
6722
6813
|
}
|
|
6723
|
-
const rows = app.db.select().from(queries).where(
|
|
6814
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6724
6815
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6725
6816
|
});
|
|
6726
6817
|
app.post("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -6730,13 +6821,13 @@ async function queryRoutes(app, opts) {
|
|
|
6730
6821
|
throw validationError('Body must contain a "keywords" array');
|
|
6731
6822
|
}
|
|
6732
6823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6733
|
-
const existing = app.db.select().from(queries).where(
|
|
6824
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6734
6825
|
const existingSet = new Set(existing.map((q) => q.query));
|
|
6735
6826
|
const added = [];
|
|
6736
6827
|
for (const keyword of body.keywords) {
|
|
6737
6828
|
if (!existingSet.has(keyword)) {
|
|
6738
6829
|
app.db.insert(queries).values({
|
|
6739
|
-
id:
|
|
6830
|
+
id: crypto6.randomUUID(),
|
|
6740
6831
|
projectId: project.id,
|
|
6741
6832
|
query: keyword,
|
|
6742
6833
|
provenance: "cli",
|
|
@@ -6755,7 +6846,7 @@ async function queryRoutes(app, opts) {
|
|
|
6755
6846
|
diff: { added }
|
|
6756
6847
|
}));
|
|
6757
6848
|
}
|
|
6758
|
-
const rows = app.db.select().from(queries).where(
|
|
6849
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6759
6850
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6760
6851
|
});
|
|
6761
6852
|
app.post("/projects/:name/keywords/generate", async (request, reply) => {
|
|
@@ -6782,7 +6873,7 @@ async function queryRoutes(app, opts) {
|
|
|
6782
6873
|
if (!opts.onGenerateQueries) {
|
|
6783
6874
|
throw notImplemented("Keyword generation is not supported in this deployment");
|
|
6784
6875
|
}
|
|
6785
|
-
const existingRows = app.db.select().from(queries).where(
|
|
6876
|
+
const existingRows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6786
6877
|
const existingQueries = existingRows.map((r) => r.query);
|
|
6787
6878
|
try {
|
|
6788
6879
|
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
@@ -6801,8 +6892,8 @@ async function queryRoutes(app, opts) {
|
|
|
6801
6892
|
}
|
|
6802
6893
|
|
|
6803
6894
|
// ../api-routes/src/competitors.ts
|
|
6804
|
-
import
|
|
6805
|
-
import { and as and2, eq as
|
|
6895
|
+
import crypto7 from "crypto";
|
|
6896
|
+
import { and as and2, eq as eq6 } from "drizzle-orm";
|
|
6806
6897
|
function normalizeCompetitor(domain) {
|
|
6807
6898
|
const reg = registrableDomain(domain);
|
|
6808
6899
|
if (reg) return reg;
|
|
@@ -6827,7 +6918,7 @@ function serializeCompetitor(row) {
|
|
|
6827
6918
|
async function competitorRoutes(app) {
|
|
6828
6919
|
app.get("/projects/:name/competitors", async (request, reply) => {
|
|
6829
6920
|
const project = resolveProject(app.db, request.params.name);
|
|
6830
|
-
const rows = app.db.select().from(competitors).where(
|
|
6921
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6831
6922
|
return reply.send(rows.map(serializeCompetitor));
|
|
6832
6923
|
});
|
|
6833
6924
|
app.put("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -6839,10 +6930,10 @@ async function competitorRoutes(app) {
|
|
|
6839
6930
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6840
6931
|
const normalizedCompetitors = normalizeCompetitorList(body.competitors);
|
|
6841
6932
|
app.db.transaction((tx) => {
|
|
6842
|
-
tx.delete(competitors).where(
|
|
6933
|
+
tx.delete(competitors).where(eq6(competitors.projectId, project.id)).run();
|
|
6843
6934
|
for (const domain of normalizedCompetitors) {
|
|
6844
6935
|
tx.insert(competitors).values({
|
|
6845
|
-
id:
|
|
6936
|
+
id: crypto7.randomUUID(),
|
|
6846
6937
|
projectId: project.id,
|
|
6847
6938
|
domain,
|
|
6848
6939
|
provenance: "cli",
|
|
@@ -6857,7 +6948,7 @@ async function competitorRoutes(app) {
|
|
|
6857
6948
|
diff: { competitors: normalizedCompetitors }
|
|
6858
6949
|
});
|
|
6859
6950
|
});
|
|
6860
|
-
const rows = app.db.select().from(competitors).where(
|
|
6951
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6861
6952
|
return reply.send(rows.map(serializeCompetitor));
|
|
6862
6953
|
});
|
|
6863
6954
|
app.post("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -6866,13 +6957,13 @@ async function competitorRoutes(app) {
|
|
|
6866
6957
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6867
6958
|
const requested = normalizeCompetitorList(body.competitors);
|
|
6868
6959
|
app.db.transaction((tx) => {
|
|
6869
|
-
const existing = tx.select().from(competitors).where(
|
|
6960
|
+
const existing = tx.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6870
6961
|
const existingSet = new Set(existing.map((c) => c.domain));
|
|
6871
6962
|
const added = requested.filter((domain) => !existingSet.has(domain));
|
|
6872
6963
|
if (added.length === 0) return;
|
|
6873
6964
|
for (const domain of added) {
|
|
6874
6965
|
tx.insert(competitors).values({
|
|
6875
|
-
id:
|
|
6966
|
+
id: crypto7.randomUUID(),
|
|
6876
6967
|
projectId: project.id,
|
|
6877
6968
|
domain,
|
|
6878
6969
|
provenance: "cli",
|
|
@@ -6889,7 +6980,7 @@ async function competitorRoutes(app) {
|
|
|
6889
6980
|
diff: { added }
|
|
6890
6981
|
});
|
|
6891
6982
|
});
|
|
6892
|
-
const rows = app.db.select().from(competitors).where(
|
|
6983
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6893
6984
|
return reply.send(rows.map(serializeCompetitor));
|
|
6894
6985
|
});
|
|
6895
6986
|
app.delete("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -6897,11 +6988,11 @@ async function competitorRoutes(app) {
|
|
|
6897
6988
|
const body = parseCompetitorBatch(request.body);
|
|
6898
6989
|
const requested = new Set(normalizeCompetitorList(body.competitors));
|
|
6899
6990
|
app.db.transaction((tx) => {
|
|
6900
|
-
const existing = tx.select().from(competitors).where(
|
|
6991
|
+
const existing = tx.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6901
6992
|
const rowsToDelete = existing.filter((c) => requested.has(c.domain));
|
|
6902
6993
|
if (rowsToDelete.length === 0) return;
|
|
6903
6994
|
for (const row of rowsToDelete) {
|
|
6904
|
-
tx.delete(competitors).where(
|
|
6995
|
+
tx.delete(competitors).where(eq6(competitors.id, row.id)).run();
|
|
6905
6996
|
}
|
|
6906
6997
|
writeAuditLog(tx, {
|
|
6907
6998
|
projectId: project.id,
|
|
@@ -6911,17 +7002,17 @@ async function competitorRoutes(app) {
|
|
|
6911
7002
|
diff: { deleted: rowsToDelete.map((row) => row.domain) }
|
|
6912
7003
|
});
|
|
6913
7004
|
});
|
|
6914
|
-
const rows = app.db.select().from(competitors).where(
|
|
7005
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6915
7006
|
return reply.send(rows.map(serializeCompetitor));
|
|
6916
7007
|
});
|
|
6917
7008
|
app.delete("/projects/:name/competitors/:id", async (request, reply) => {
|
|
6918
7009
|
const project = resolveProject(app.db, request.params.name);
|
|
6919
|
-
const competitor = app.db.select().from(competitors).where(and2(
|
|
7010
|
+
const competitor = app.db.select().from(competitors).where(and2(eq6(competitors.projectId, project.id), eq6(competitors.id, request.params.id))).get();
|
|
6920
7011
|
if (!competitor) {
|
|
6921
7012
|
throw notFound("Competitor", request.params.id);
|
|
6922
7013
|
}
|
|
6923
7014
|
app.db.transaction((tx) => {
|
|
6924
|
-
tx.delete(competitors).where(
|
|
7015
|
+
tx.delete(competitors).where(eq6(competitors.id, competitor.id)).run();
|
|
6925
7016
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6926
7017
|
projectId: project.id,
|
|
6927
7018
|
actor: "api",
|
|
@@ -6946,23 +7037,23 @@ function parseCompetitorBatch(value) {
|
|
|
6946
7037
|
}
|
|
6947
7038
|
|
|
6948
7039
|
// ../api-routes/src/runs.ts
|
|
6949
|
-
import
|
|
6950
|
-
import { and as and4, eq as
|
|
7040
|
+
import crypto9 from "crypto";
|
|
7041
|
+
import { and as and4, eq as eq8, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
|
|
6951
7042
|
import { gte } from "drizzle-orm";
|
|
6952
7043
|
|
|
6953
7044
|
// ../api-routes/src/run-queue.ts
|
|
6954
|
-
import
|
|
6955
|
-
import { and as and3, eq as
|
|
7045
|
+
import crypto8 from "crypto";
|
|
7046
|
+
import { and as and3, eq as eq7, or } from "drizzle-orm";
|
|
6956
7047
|
function queueRunIfProjectIdle(db, params) {
|
|
6957
7048
|
const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6958
7049
|
const kind = params.kind ?? "answer-visibility";
|
|
6959
7050
|
const trigger = params.trigger ?? "manual";
|
|
6960
|
-
const runId =
|
|
7051
|
+
const runId = crypto8.randomUUID();
|
|
6961
7052
|
return db.transaction((tx) => {
|
|
6962
7053
|
const activeRun = tx.select().from(runs).where(
|
|
6963
7054
|
and3(
|
|
6964
|
-
|
|
6965
|
-
or(
|
|
7055
|
+
eq7(runs.projectId, params.projectId),
|
|
7056
|
+
or(eq7(runs.status, "queued"), eq7(runs.status, "running"))
|
|
6966
7057
|
)
|
|
6967
7058
|
).get();
|
|
6968
7059
|
if (activeRun) {
|
|
@@ -7008,7 +7099,7 @@ async function runRoutes(app, opts) {
|
|
|
7008
7099
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
7009
7100
|
let scopedQueries = null;
|
|
7010
7101
|
if (body.queries?.length) {
|
|
7011
|
-
const trackedRows = app.db.select({ query: queries.query }).from(queries).where(
|
|
7102
|
+
const trackedRows = app.db.select({ query: queries.query }).from(queries).where(eq8(queries.projectId, project.id)).all();
|
|
7012
7103
|
const tracked = new Set(trackedRows.map((r) => r.query));
|
|
7013
7104
|
const missing = body.queries.filter((q) => !tracked.has(q));
|
|
7014
7105
|
if (missing.length) {
|
|
@@ -7044,15 +7135,15 @@ async function runRoutes(app, opts) {
|
|
|
7044
7135
|
}
|
|
7045
7136
|
const result = app.db.transaction((tx) => {
|
|
7046
7137
|
const activeRun = tx.select({ id: runs.id }).from(runs).where(and4(
|
|
7047
|
-
|
|
7048
|
-
or2(
|
|
7138
|
+
eq8(runs.projectId, project.id),
|
|
7139
|
+
or2(eq8(runs.status, "queued"), eq8(runs.status, "running"))
|
|
7049
7140
|
)).get();
|
|
7050
7141
|
if (activeRun) {
|
|
7051
7142
|
return { conflict: true };
|
|
7052
7143
|
}
|
|
7053
7144
|
const inserted = [];
|
|
7054
7145
|
for (const loc of projectLocations) {
|
|
7055
|
-
const runId2 =
|
|
7146
|
+
const runId2 = crypto9.randomUUID();
|
|
7056
7147
|
tx.insert(runs).values({
|
|
7057
7148
|
id: runId2,
|
|
7058
7149
|
projectId: project.id,
|
|
@@ -7079,7 +7170,7 @@ async function runRoutes(app, opts) {
|
|
|
7079
7170
|
entityType: "run",
|
|
7080
7171
|
entityId: runId2
|
|
7081
7172
|
});
|
|
7082
|
-
const r = app.db.select().from(runs).where(
|
|
7173
|
+
const r = app.db.select().from(runs).where(eq8(runs.id, runId2)).get();
|
|
7083
7174
|
if (opts.onRunCreated) {
|
|
7084
7175
|
opts.onRunCreated(runId2, project.id, providers, loc);
|
|
7085
7176
|
}
|
|
@@ -7105,7 +7196,7 @@ async function runRoutes(app, opts) {
|
|
|
7105
7196
|
entityType: "run",
|
|
7106
7197
|
entityId: runId
|
|
7107
7198
|
});
|
|
7108
|
-
const run = app.db.select().from(runs).where(
|
|
7199
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, runId)).get();
|
|
7109
7200
|
if (opts.onRunCreated) {
|
|
7110
7201
|
opts.onRunCreated(runId, project.id, providers, resolvedLocation);
|
|
7111
7202
|
}
|
|
@@ -7116,15 +7207,15 @@ async function runRoutes(app, opts) {
|
|
|
7116
7207
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
7117
7208
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
|
|
7118
7209
|
const kind = parseListKind(request.query.kind);
|
|
7119
|
-
const where = kind ? and4(
|
|
7210
|
+
const where = kind ? and4(eq8(runs.projectId, project.id), eq8(runs.kind, kind)) : eq8(runs.projectId, project.id);
|
|
7120
7211
|
const rows = limit == null ? app.db.select().from(runs).where(where).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(where).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
|
|
7121
7212
|
return reply.send(rows.map(formatRun));
|
|
7122
7213
|
});
|
|
7123
7214
|
app.get("/projects/:name/runs/latest", async (request, reply) => {
|
|
7124
7215
|
const project = resolveProject(app.db, request.params.name);
|
|
7125
|
-
const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(
|
|
7216
|
+
const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).get();
|
|
7126
7217
|
const totalRuns = countRow?.count ?? 0;
|
|
7127
|
-
const latestRun = app.db.select().from(runs).where(and4(
|
|
7218
|
+
const latestRun = app.db.select().from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
|
|
7128
7219
|
if (!latestRun) {
|
|
7129
7220
|
return reply.send({ totalRuns: 0, run: null });
|
|
7130
7221
|
}
|
|
@@ -7140,15 +7231,15 @@ async function runRoutes(app, opts) {
|
|
|
7140
7231
|
const kind = parseListKind(request.query.kind);
|
|
7141
7232
|
const filters = [gte(runs.createdAt, since)];
|
|
7142
7233
|
if (!includeProbe) filters.push(notProbeRun());
|
|
7143
|
-
if (kind) filters.push(
|
|
7234
|
+
if (kind) filters.push(eq8(runs.kind, kind));
|
|
7144
7235
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7145
|
-
if (scopedProjectId) filters.push(
|
|
7236
|
+
if (scopedProjectId) filters.push(eq8(runs.projectId, scopedProjectId));
|
|
7146
7237
|
const rows = app.db.select().from(runs).where(and4(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
|
|
7147
7238
|
return reply.send(rows.map(formatRun));
|
|
7148
7239
|
});
|
|
7149
7240
|
app.post("/runs", async (request, reply) => {
|
|
7150
7241
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7151
|
-
const allProjects = (scopedProjectId ? app.db.select().from(projects).where(
|
|
7242
|
+
const allProjects = (scopedProjectId ? app.db.select().from(projects).where(eq8(projects.id, scopedProjectId)) : app.db.select().from(projects)).all();
|
|
7152
7243
|
if (allProjects.length === 0) {
|
|
7153
7244
|
return reply.status(207).send([]);
|
|
7154
7245
|
}
|
|
@@ -7203,7 +7294,7 @@ async function runRoutes(app, opts) {
|
|
|
7203
7294
|
entityType: "run",
|
|
7204
7295
|
entityId: runId
|
|
7205
7296
|
});
|
|
7206
|
-
const run = app.db.select().from(runs).where(
|
|
7297
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, runId)).get();
|
|
7207
7298
|
if (opts.onRunCreated) {
|
|
7208
7299
|
opts.onRunCreated(runId, project.id, providers, resolvedLocation);
|
|
7209
7300
|
}
|
|
@@ -7212,13 +7303,13 @@ async function runRoutes(app, opts) {
|
|
|
7212
7303
|
return reply.status(207).send(results);
|
|
7213
7304
|
});
|
|
7214
7305
|
app.post("/runs/:id/cancel", async (request, reply) => {
|
|
7215
|
-
const run = app.db.select().from(runs).where(
|
|
7306
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, request.params.id)).get();
|
|
7216
7307
|
if (!run) throw notFound("Run", request.params.id);
|
|
7217
7308
|
assertProjectScope(request, run.projectId);
|
|
7218
7309
|
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
7219
7310
|
if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
|
|
7220
7311
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7221
|
-
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(
|
|
7312
|
+
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(eq8(runs.id, run.id)).run();
|
|
7222
7313
|
writeAuditLog(app.db, {
|
|
7223
7314
|
projectId: run.projectId,
|
|
7224
7315
|
actor: "api",
|
|
@@ -7226,11 +7317,11 @@ async function runRoutes(app, opts) {
|
|
|
7226
7317
|
entityType: "run",
|
|
7227
7318
|
entityId: run.id
|
|
7228
7319
|
});
|
|
7229
|
-
const updated = app.db.select().from(runs).where(
|
|
7320
|
+
const updated = app.db.select().from(runs).where(eq8(runs.id, run.id)).get();
|
|
7230
7321
|
return reply.send(formatRun(updated));
|
|
7231
7322
|
});
|
|
7232
7323
|
app.get("/runs/:id", async (request, reply) => {
|
|
7233
|
-
const run = app.db.select().from(runs).where(
|
|
7324
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, request.params.id)).get();
|
|
7234
7325
|
if (!run) throw notFound("Run", request.params.id);
|
|
7235
7326
|
assertProjectScope(request, run.projectId);
|
|
7236
7327
|
return reply.send(loadRunDetail(app, run));
|
|
@@ -7302,7 +7393,7 @@ function loadRunDetail(app, run) {
|
|
|
7302
7393
|
canonicalDomain: projects.canonicalDomain,
|
|
7303
7394
|
ownedDomains: projects.ownedDomains,
|
|
7304
7395
|
aliases: projects.aliases
|
|
7305
|
-
}).from(projects).where(
|
|
7396
|
+
}).from(projects).where(eq8(projects.id, run.projectId)).get();
|
|
7306
7397
|
const snapshots = app.db.select({
|
|
7307
7398
|
id: querySnapshots.id,
|
|
7308
7399
|
runId: querySnapshots.runId,
|
|
@@ -7319,7 +7410,7 @@ function loadRunDetail(app, run) {
|
|
|
7319
7410
|
location: querySnapshots.location,
|
|
7320
7411
|
rawResponse: querySnapshots.rawResponse,
|
|
7321
7412
|
createdAt: querySnapshots.createdAt
|
|
7322
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
7413
|
+
}).from(querySnapshots).leftJoin(queries, eq8(querySnapshots.queryId, queries.id)).where(eq8(querySnapshots.runId, run.id)).all();
|
|
7323
7414
|
return {
|
|
7324
7415
|
...formatRun(run),
|
|
7325
7416
|
snapshots: snapshots.map((s) => {
|
|
@@ -7353,8 +7444,8 @@ function loadRunDetail(app, run) {
|
|
|
7353
7444
|
}
|
|
7354
7445
|
|
|
7355
7446
|
// ../api-routes/src/apply.ts
|
|
7356
|
-
import
|
|
7357
|
-
import { and as and5, eq as
|
|
7447
|
+
import crypto11 from "crypto";
|
|
7448
|
+
import { and as and5, eq as eq9 } from "drizzle-orm";
|
|
7358
7449
|
|
|
7359
7450
|
// ../api-routes/src/schedule-utils.ts
|
|
7360
7451
|
import { CronExpressionParser } from "cron-parser";
|
|
@@ -7457,7 +7548,7 @@ function nextRunFromCron(cronExpr, timezone, from = /* @__PURE__ */ new Date())
|
|
|
7457
7548
|
}
|
|
7458
7549
|
|
|
7459
7550
|
// ../api-routes/src/webhooks.ts
|
|
7460
|
-
import
|
|
7551
|
+
import crypto10 from "crypto";
|
|
7461
7552
|
import dns from "dns/promises";
|
|
7462
7553
|
import http from "http";
|
|
7463
7554
|
import https from "https";
|
|
@@ -7509,7 +7600,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
7509
7600
|
"User-Agent": "Canonry/0.1.0"
|
|
7510
7601
|
};
|
|
7511
7602
|
if (webhookSecret) {
|
|
7512
|
-
headers["X-Canonry-Signature"] = "sha256=" +
|
|
7603
|
+
headers["X-Canonry-Signature"] = "sha256=" + crypto10.createHmac("sha256", webhookSecret).update(body).digest("hex");
|
|
7513
7604
|
}
|
|
7514
7605
|
return await new Promise((resolve) => {
|
|
7515
7606
|
const requestOptions = {
|
|
@@ -7690,7 +7781,7 @@ async function applyRoutes(app, opts) {
|
|
|
7690
7781
|
const configQueries = resolveConfigSpecQueries(config.spec);
|
|
7691
7782
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7692
7783
|
if (scopedProjectId) {
|
|
7693
|
-
const target = app.db.select({ id: projects.id }).from(projects).where(
|
|
7784
|
+
const target = app.db.select({ id: projects.id }).from(projects).where(eq9(projects.name, name)).get();
|
|
7694
7785
|
if (!target || target.id !== scopedProjectId) {
|
|
7695
7786
|
throw forbidden("This API key is scoped to a single project and cannot apply this config.");
|
|
7696
7787
|
}
|
|
@@ -7699,7 +7790,7 @@ async function applyRoutes(app, opts) {
|
|
|
7699
7790
|
let scheduleAction = null;
|
|
7700
7791
|
let aliasesChanged = false;
|
|
7701
7792
|
app.db.transaction((tx) => {
|
|
7702
|
-
const existing = tx.select().from(projects).where(
|
|
7793
|
+
const existing = tx.select().from(projects).where(eq9(projects.name, name)).get();
|
|
7703
7794
|
const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
|
|
7704
7795
|
if (existing) {
|
|
7705
7796
|
const prevAliases = existing.aliases;
|
|
@@ -7722,7 +7813,7 @@ async function applyRoutes(app, opts) {
|
|
|
7722
7813
|
configSource: "config-file",
|
|
7723
7814
|
configRevision: existing.configRevision + 1,
|
|
7724
7815
|
updatedAt: now
|
|
7725
|
-
}).where(
|
|
7816
|
+
}).where(eq9(projects.id, existing.id)).run();
|
|
7726
7817
|
writeAuditLog(tx, {
|
|
7727
7818
|
projectId,
|
|
7728
7819
|
actor: "api",
|
|
@@ -7731,7 +7822,7 @@ async function applyRoutes(app, opts) {
|
|
|
7731
7822
|
entityId: projectId
|
|
7732
7823
|
});
|
|
7733
7824
|
} else {
|
|
7734
|
-
projectId =
|
|
7825
|
+
projectId = crypto11.randomUUID();
|
|
7735
7826
|
tx.insert(projects).values({
|
|
7736
7827
|
id: projectId,
|
|
7737
7828
|
name,
|
|
@@ -7760,16 +7851,7 @@ async function applyRoutes(app, opts) {
|
|
|
7760
7851
|
entityId: projectId
|
|
7761
7852
|
});
|
|
7762
7853
|
}
|
|
7763
|
-
tx
|
|
7764
|
-
for (const q of configQueries) {
|
|
7765
|
-
tx.insert(queries).values({
|
|
7766
|
-
id: crypto10.randomUUID(),
|
|
7767
|
-
projectId,
|
|
7768
|
-
query: q,
|
|
7769
|
-
provenance: "cli",
|
|
7770
|
-
createdAt: now
|
|
7771
|
-
}).run();
|
|
7772
|
-
}
|
|
7854
|
+
replaceProjectQueries(tx, projectId, configQueries, now);
|
|
7773
7855
|
writeAuditLog(tx, {
|
|
7774
7856
|
projectId,
|
|
7775
7857
|
actor: "api",
|
|
@@ -7777,11 +7859,11 @@ async function applyRoutes(app, opts) {
|
|
|
7777
7859
|
entityType: "query",
|
|
7778
7860
|
diff: { queries: configQueries }
|
|
7779
7861
|
});
|
|
7780
|
-
tx.delete(competitors).where(
|
|
7862
|
+
tx.delete(competitors).where(eq9(competitors.projectId, projectId)).run();
|
|
7781
7863
|
const normalizedCompetitors = normalizeCompetitorList2(config.spec.competitors);
|
|
7782
7864
|
for (const domain of normalizedCompetitors) {
|
|
7783
7865
|
tx.insert(competitors).values({
|
|
7784
|
-
id:
|
|
7866
|
+
id: crypto11.randomUUID(),
|
|
7785
7867
|
projectId,
|
|
7786
7868
|
domain,
|
|
7787
7869
|
provenance: "cli",
|
|
@@ -7797,7 +7879,7 @@ async function applyRoutes(app, opts) {
|
|
|
7797
7879
|
});
|
|
7798
7880
|
const AV_KIND = SchedulableRunKinds["answer-visibility"];
|
|
7799
7881
|
if (resolvedSchedule) {
|
|
7800
|
-
const existingSched = tx.select().from(schedules).where(and5(
|
|
7882
|
+
const existingSched = tx.select().from(schedules).where(and5(eq9(schedules.projectId, projectId), eq9(schedules.kind, AV_KIND))).get();
|
|
7801
7883
|
if (existingSched) {
|
|
7802
7884
|
tx.update(schedules).set({
|
|
7803
7885
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -7806,10 +7888,10 @@ async function applyRoutes(app, opts) {
|
|
|
7806
7888
|
providers: config.spec.schedule?.providers ?? [],
|
|
7807
7889
|
enabled: true,
|
|
7808
7890
|
updatedAt: now
|
|
7809
|
-
}).where(
|
|
7891
|
+
}).where(eq9(schedules.id, existingSched.id)).run();
|
|
7810
7892
|
} else {
|
|
7811
7893
|
tx.insert(schedules).values({
|
|
7812
|
-
id:
|
|
7894
|
+
id: crypto11.randomUUID(),
|
|
7813
7895
|
projectId,
|
|
7814
7896
|
kind: AV_KIND,
|
|
7815
7897
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -7823,21 +7905,21 @@ async function applyRoutes(app, opts) {
|
|
|
7823
7905
|
}
|
|
7824
7906
|
scheduleAction = "upsert";
|
|
7825
7907
|
} else if (deleteSchedule) {
|
|
7826
|
-
const existingSched = tx.select().from(schedules).where(and5(
|
|
7908
|
+
const existingSched = tx.select().from(schedules).where(and5(eq9(schedules.projectId, projectId), eq9(schedules.kind, AV_KIND))).get();
|
|
7827
7909
|
if (existingSched) {
|
|
7828
|
-
tx.delete(schedules).where(and5(
|
|
7910
|
+
tx.delete(schedules).where(and5(eq9(schedules.projectId, projectId), eq9(schedules.kind, AV_KIND))).run();
|
|
7829
7911
|
scheduleAction = "delete";
|
|
7830
7912
|
}
|
|
7831
7913
|
}
|
|
7832
7914
|
if (hasNotifications) {
|
|
7833
|
-
tx.delete(notifications).where(
|
|
7915
|
+
tx.delete(notifications).where(eq9(notifications.projectId, projectId)).run();
|
|
7834
7916
|
for (const notif of config.spec.notifications) {
|
|
7835
7917
|
tx.insert(notifications).values({
|
|
7836
|
-
id:
|
|
7918
|
+
id: crypto11.randomUUID(),
|
|
7837
7919
|
projectId,
|
|
7838
7920
|
channel: notif.channel,
|
|
7839
7921
|
config: { url: notif.url, events: notif.events },
|
|
7840
|
-
webhookSecret:
|
|
7922
|
+
webhookSecret: crypto11.randomBytes(32).toString("hex"),
|
|
7841
7923
|
enabled: true,
|
|
7842
7924
|
createdAt: now,
|
|
7843
7925
|
updatedAt: now
|
|
@@ -7864,7 +7946,7 @@ async function applyRoutes(app, opts) {
|
|
|
7864
7946
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
7865
7947
|
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
7866
7948
|
}
|
|
7867
|
-
const project = app.db.select().from(projects).where(
|
|
7949
|
+
const project = app.db.select().from(projects).where(eq9(projects.id, projectId)).get();
|
|
7868
7950
|
return reply.status(200).send({
|
|
7869
7951
|
id: project.id,
|
|
7870
7952
|
name: project.name,
|
|
@@ -7909,7 +7991,7 @@ function normalizeCompetitorList2(domains) {
|
|
|
7909
7991
|
}
|
|
7910
7992
|
|
|
7911
7993
|
// ../api-routes/src/history.ts
|
|
7912
|
-
import { and as and6, eq as
|
|
7994
|
+
import { and as and6, eq as eq10, desc as desc2, inArray as inArray3 } from "drizzle-orm";
|
|
7913
7995
|
|
|
7914
7996
|
// ../api-routes/src/notification-redaction.ts
|
|
7915
7997
|
var REDACTED_URL = {
|
|
@@ -7955,19 +8037,19 @@ function redactNotificationDiff(value) {
|
|
|
7955
8037
|
async function historyRoutes(app) {
|
|
7956
8038
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
7957
8039
|
const project = resolveProject(app.db, request.params.name);
|
|
7958
|
-
const rows = app.db.select().from(auditLog).where(
|
|
8040
|
+
const rows = app.db.select().from(auditLog).where(eq10(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
7959
8041
|
return reply.send(rows.map(formatAuditEntry));
|
|
7960
8042
|
});
|
|
7961
8043
|
app.get("/history", async (request, reply) => {
|
|
7962
8044
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7963
|
-
const rows = app.db.select().from(auditLog).where(scopedProjectId ?
|
|
8045
|
+
const rows = app.db.select().from(auditLog).where(scopedProjectId ? eq10(auditLog.projectId, scopedProjectId) : void 0).orderBy(desc2(auditLog.createdAt)).all();
|
|
7964
8046
|
return reply.send(rows.map(formatAuditEntry));
|
|
7965
8047
|
});
|
|
7966
8048
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
7967
8049
|
const project = resolveProject(app.db, request.params.name);
|
|
7968
8050
|
const limit = parseInt(request.query.limit ?? "50", 10);
|
|
7969
8051
|
const offset = parseInt(request.query.offset ?? "0", 10);
|
|
7970
|
-
const projectRuns = app.db.select({ id: runs.id }).from(runs).where(and6(
|
|
8052
|
+
const projectRuns = app.db.select({ id: runs.id }).from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).all();
|
|
7971
8053
|
if (projectRuns.length === 0) {
|
|
7972
8054
|
return reply.send({ snapshots: [], total: 0 });
|
|
7973
8055
|
}
|
|
@@ -7986,7 +8068,7 @@ async function historyRoutes(app) {
|
|
|
7986
8068
|
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
7987
8069
|
location: querySnapshots.location,
|
|
7988
8070
|
createdAt: querySnapshots.createdAt
|
|
7989
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8071
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray3(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
|
|
7990
8072
|
const locationFilter = request.query.location;
|
|
7991
8073
|
const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
|
|
7992
8074
|
const total = filtered.length;
|
|
@@ -8015,13 +8097,13 @@ async function historyRoutes(app) {
|
|
|
8015
8097
|
});
|
|
8016
8098
|
app.get("/projects/:name/timeline", async (request, reply) => {
|
|
8017
8099
|
const project = resolveProject(app.db, request.params.name);
|
|
8018
|
-
const projectQueries = app.db.select().from(queries).where(
|
|
8019
|
-
const projectRuns = app.db.select().from(runs).where(and6(
|
|
8100
|
+
const projectQueries = app.db.select().from(queries).where(eq10(queries.projectId, project.id)).all();
|
|
8101
|
+
const projectRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all();
|
|
8020
8102
|
if (projectRuns.length === 0 || projectQueries.length === 0) {
|
|
8021
8103
|
return reply.send([]);
|
|
8022
8104
|
}
|
|
8023
8105
|
const runIds = new Set(projectRuns.map((r) => r.id));
|
|
8024
|
-
const rawSnapshots = app.db.select().from(querySnapshots).where(
|
|
8106
|
+
const rawSnapshots = app.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, [...runIds])).all();
|
|
8025
8107
|
const timelineLocationFilter = request.query.location;
|
|
8026
8108
|
const filteredSnapshots = timelineLocationFilter !== void 0 ? rawSnapshots.filter((s) => s.location === (timelineLocationFilter || null)) : rawSnapshots;
|
|
8027
8109
|
const allSnapshots = filteredSnapshots.map((snapshot) => ({
|
|
@@ -8142,7 +8224,7 @@ async function historyRoutes(app) {
|
|
|
8142
8224
|
throw validationError("Both run1 and run2 query params are required");
|
|
8143
8225
|
}
|
|
8144
8226
|
const requestedRunIds = [.../* @__PURE__ */ new Set([run1, run2])];
|
|
8145
|
-
const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
8227
|
+
const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray3(runs.id, requestedRunIds)).all();
|
|
8146
8228
|
const runsById = new Map(runRows.map((row) => [row.id, row]));
|
|
8147
8229
|
for (const runId of requestedRunIds) {
|
|
8148
8230
|
const run = runsById.get(runId);
|
|
@@ -8156,14 +8238,14 @@ async function historyRoutes(app) {
|
|
|
8156
8238
|
citationState: querySnapshots.citationState,
|
|
8157
8239
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8158
8240
|
answerText: querySnapshots.answerText
|
|
8159
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8241
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run1)).all();
|
|
8160
8242
|
const snaps2 = app.db.select({
|
|
8161
8243
|
queryId: querySnapshots.queryId,
|
|
8162
8244
|
query: queries.query,
|
|
8163
8245
|
citationState: querySnapshots.citationState,
|
|
8164
8246
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8165
8247
|
answerText: querySnapshots.answerText
|
|
8166
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8248
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run2)).all();
|
|
8167
8249
|
const map1 = /* @__PURE__ */ new Map();
|
|
8168
8250
|
for (const s of snaps1) {
|
|
8169
8251
|
const resolved = {
|
|
@@ -8227,13 +8309,13 @@ function formatAuditEntry(row) {
|
|
|
8227
8309
|
}
|
|
8228
8310
|
|
|
8229
8311
|
// ../api-routes/src/analytics.ts
|
|
8230
|
-
import { and as and7, eq as
|
|
8312
|
+
import { and as and7, eq as eq11, desc as desc3, inArray as inArray4 } from "drizzle-orm";
|
|
8231
8313
|
async function analyticsRoutes(app) {
|
|
8232
8314
|
app.get("/projects/:name/analytics/metrics", async (request, reply) => {
|
|
8233
8315
|
const project = resolveProject(app.db, request.params.name);
|
|
8234
8316
|
const window = parseWindow(request.query.window);
|
|
8235
8317
|
const cutoff = windowCutoff(window);
|
|
8236
|
-
const projectRuns = app.db.select().from(runs).where(and7(
|
|
8318
|
+
const projectRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8237
8319
|
if (projectRuns.length === 0) {
|
|
8238
8320
|
return reply.send({
|
|
8239
8321
|
window,
|
|
@@ -8254,14 +8336,14 @@ async function analyticsRoutes(app) {
|
|
|
8254
8336
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8255
8337
|
answerText: querySnapshots.answerText,
|
|
8256
8338
|
createdAt: querySnapshots.createdAt
|
|
8257
|
-
}).from(querySnapshots).where(
|
|
8339
|
+
}).from(querySnapshots).where(inArray4(querySnapshots.runId, runIds)).all());
|
|
8258
8340
|
const allSnapshots = rawSnapshots.map((s) => ({
|
|
8259
8341
|
...s,
|
|
8260
8342
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
8261
8343
|
}));
|
|
8262
|
-
const projectQueries = app.db.select({ id: queries.id, createdAt: queries.createdAt }).from(queries).where(
|
|
8344
|
+
const projectQueries = app.db.select({ id: queries.id, createdAt: queries.createdAt }).from(queries).where(eq11(queries.projectId, project.id)).all();
|
|
8263
8345
|
const queryCreatedAt = new Map(projectQueries.map((q) => [q.id, q.createdAt]));
|
|
8264
|
-
const mentionShareCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
8346
|
+
const mentionShareCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, project.id)).all().map((c) => ({
|
|
8265
8347
|
domain: c.domain,
|
|
8266
8348
|
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
8267
8349
|
}));
|
|
@@ -8285,14 +8367,14 @@ async function analyticsRoutes(app) {
|
|
|
8285
8367
|
const project = resolveProject(app.db, request.params.name);
|
|
8286
8368
|
const window = parseWindow(request.query.window);
|
|
8287
8369
|
const cutoff = windowCutoff(window);
|
|
8288
|
-
const completedRuns = app.db.select().from(runs).where(and7(
|
|
8370
|
+
const completedRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
|
|
8289
8371
|
const latestGroup = groupRunsByCreatedAt(completedRuns)[0] ?? [];
|
|
8290
8372
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
8291
8373
|
const latestRun = pickGroupRepresentative(latestGroup);
|
|
8292
8374
|
if (!latestRun) {
|
|
8293
8375
|
return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
|
|
8294
8376
|
}
|
|
8295
|
-
const windowRuns = app.db.select().from(runs).where(and7(
|
|
8377
|
+
const windowRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8296
8378
|
const windowRunIds = windowRuns.map((r) => r.id);
|
|
8297
8379
|
const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
|
|
8298
8380
|
const consistencyMap = /* @__PURE__ */ new Map();
|
|
@@ -8303,7 +8385,7 @@ async function analyticsRoutes(app) {
|
|
|
8303
8385
|
citationState: querySnapshots.citationState,
|
|
8304
8386
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8305
8387
|
answerText: querySnapshots.answerText
|
|
8306
|
-
}).from(querySnapshots).where(
|
|
8388
|
+
}).from(querySnapshots).where(inArray4(querySnapshots.runId, windowRunIds)).all());
|
|
8307
8389
|
for (const s of allWindowSnaps) {
|
|
8308
8390
|
const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
|
|
8309
8391
|
let entry = consistencyMap.get(s.queryId);
|
|
@@ -8324,7 +8406,7 @@ async function analyticsRoutes(app) {
|
|
|
8324
8406
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8325
8407
|
answerText: querySnapshots.answerText,
|
|
8326
8408
|
competitorOverlap: querySnapshots.competitorOverlap
|
|
8327
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8409
|
+
}).from(querySnapshots).leftJoin(queries, eq11(querySnapshots.queryId, queries.id)).where(inArray4(querySnapshots.runId, latestGroupRunIds)).all());
|
|
8328
8410
|
const snapshots = rawSnapshots.map((s) => ({
|
|
8329
8411
|
...s,
|
|
8330
8412
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
@@ -8416,14 +8498,14 @@ async function analyticsRoutes(app) {
|
|
|
8416
8498
|
}
|
|
8417
8499
|
const classifyCtx = {
|
|
8418
8500
|
projectDomains: effectiveDomains(project),
|
|
8419
|
-
competitorDomains: app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
8501
|
+
competitorDomains: app.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, project.id)).all().map((r) => r.domain)
|
|
8420
8502
|
};
|
|
8421
8503
|
const storedSurfaceClasses = /* @__PURE__ */ new Map();
|
|
8422
|
-
for (const row of app.db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(
|
|
8504
|
+
for (const row of app.db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(eq11(domainClassifications.projectId, project.id)).all()) {
|
|
8423
8505
|
const mapped = surfaceClassFromCompetitorType(row.competitorType);
|
|
8424
8506
|
if (mapped) storedSurfaceClasses.set(normalizeProjectDomain(row.domain), mapped);
|
|
8425
8507
|
}
|
|
8426
|
-
const windowRuns = app.db.select().from(runs).where(and7(
|
|
8508
|
+
const windowRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8427
8509
|
if (windowRuns.length === 0) {
|
|
8428
8510
|
return reply.send({
|
|
8429
8511
|
overall: [],
|
|
@@ -8443,7 +8525,7 @@ async function analyticsRoutes(app) {
|
|
|
8443
8525
|
query: queries.query,
|
|
8444
8526
|
provider: querySnapshots.provider,
|
|
8445
8527
|
rawResponse: querySnapshots.rawResponse
|
|
8446
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8528
|
+
}).from(querySnapshots).leftJoin(queries, eq11(querySnapshots.queryId, queries.id)).where(inArray4(querySnapshots.runId, windowRunIds)).all();
|
|
8447
8529
|
const overallCounts = /* @__PURE__ */ new Map();
|
|
8448
8530
|
const byQuery = {};
|
|
8449
8531
|
const overallDomains = /* @__PURE__ */ new Map();
|
|
@@ -8688,7 +8770,7 @@ function buildCategoryCounts(counts2) {
|
|
|
8688
8770
|
}
|
|
8689
8771
|
|
|
8690
8772
|
// ../api-routes/src/intelligence.ts
|
|
8691
|
-
import { eq as
|
|
8773
|
+
import { eq as eq12, desc as desc4, and as and8, inArray as inArray5, like } from "drizzle-orm";
|
|
8692
8774
|
var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
8693
8775
|
function severitiesAtOrAbove(min) {
|
|
8694
8776
|
const floor = SEVERITY_RANK[min];
|
|
@@ -8806,19 +8888,19 @@ function aggregateHealthSnapshots(projectId, rows) {
|
|
|
8806
8888
|
async function intelligenceRoutes(app) {
|
|
8807
8889
|
app.get("/projects/:name/insights", async (request, reply) => {
|
|
8808
8890
|
const project = resolveProject(app.db, request.params.name);
|
|
8809
|
-
const conditions = [
|
|
8891
|
+
const conditions = [eq12(insights.projectId, project.id)];
|
|
8810
8892
|
if (request.query.runId) {
|
|
8811
|
-
conditions.push(
|
|
8893
|
+
conditions.push(eq12(insights.runId, request.query.runId));
|
|
8812
8894
|
}
|
|
8813
8895
|
const typeFilter = request.query.type?.trim();
|
|
8814
8896
|
if (typeFilter) {
|
|
8815
8897
|
conditions.push(
|
|
8816
|
-
typeFilter.endsWith("*") ? like(insights.type, `${typeFilter.slice(0, -1)}%`) :
|
|
8898
|
+
typeFilter.endsWith("*") ? like(insights.type, `${typeFilter.slice(0, -1)}%`) : eq12(insights.type, typeFilter)
|
|
8817
8899
|
);
|
|
8818
8900
|
}
|
|
8819
8901
|
const severityFilter = request.query.severity?.trim();
|
|
8820
8902
|
if (severityFilter) {
|
|
8821
|
-
conditions.push(
|
|
8903
|
+
conditions.push(inArray5(insights.severity, severitiesAtOrAbove(severityFilter)));
|
|
8822
8904
|
}
|
|
8823
8905
|
let limit;
|
|
8824
8906
|
if (request.query.limit !== void 0) {
|
|
@@ -8836,7 +8918,7 @@ async function intelligenceRoutes(app) {
|
|
|
8836
8918
|
});
|
|
8837
8919
|
app.get("/projects/:name/insights/:id", async (request, reply) => {
|
|
8838
8920
|
const project = resolveProject(app.db, request.params.name);
|
|
8839
|
-
const row = app.db.select().from(insights).where(
|
|
8921
|
+
const row = app.db.select().from(insights).where(eq12(insights.id, request.params.id)).get();
|
|
8840
8922
|
if (!row || row.projectId !== project.id) {
|
|
8841
8923
|
throw notFound("Insight", request.params.id);
|
|
8842
8924
|
}
|
|
@@ -8844,19 +8926,19 @@ async function intelligenceRoutes(app) {
|
|
|
8844
8926
|
});
|
|
8845
8927
|
app.post("/projects/:name/insights/:id/dismiss", async (request, reply) => {
|
|
8846
8928
|
const project = resolveProject(app.db, request.params.name);
|
|
8847
|
-
const row = app.db.select().from(insights).where(
|
|
8929
|
+
const row = app.db.select().from(insights).where(eq12(insights.id, request.params.id)).get();
|
|
8848
8930
|
if (!row || row.projectId !== project.id) {
|
|
8849
8931
|
throw notFound("Insight", request.params.id);
|
|
8850
8932
|
}
|
|
8851
|
-
app.db.update(insights).set({ dismissed: true }).where(
|
|
8933
|
+
app.db.update(insights).set({ dismissed: true }).where(eq12(insights.id, request.params.id)).run();
|
|
8852
8934
|
return reply.send({ ok: true });
|
|
8853
8935
|
});
|
|
8854
8936
|
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
8855
8937
|
const project = resolveProject(app.db, request.params.name);
|
|
8856
8938
|
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and8(
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8939
|
+
eq12(runs.projectId, project.id),
|
|
8940
|
+
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
8941
|
+
inArray5(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
8860
8942
|
// Health-latest is the dashboard headline; probe runs must not
|
|
8861
8943
|
// displace the most recent real visibility sweep.
|
|
8862
8944
|
notProbeRun()
|
|
@@ -8865,14 +8947,14 @@ async function intelligenceRoutes(app) {
|
|
|
8865
8947
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
8866
8948
|
if (latestGroupRunIds.length > 0) {
|
|
8867
8949
|
const groupRows = app.db.select().from(healthSnapshots).where(and8(
|
|
8868
|
-
|
|
8869
|
-
|
|
8950
|
+
eq12(healthSnapshots.projectId, project.id),
|
|
8951
|
+
inArray5(healthSnapshots.runId, latestGroupRunIds)
|
|
8870
8952
|
)).all();
|
|
8871
8953
|
if (groupRows.length > 0) {
|
|
8872
8954
|
return reply.send(aggregateHealthSnapshots(project.id, groupRows));
|
|
8873
8955
|
}
|
|
8874
8956
|
}
|
|
8875
|
-
const fallback = app.db.select().from(healthSnapshots).where(
|
|
8957
|
+
const fallback = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
8876
8958
|
if (!fallback) {
|
|
8877
8959
|
return reply.send(emptyHealthSnapshot(project.id));
|
|
8878
8960
|
}
|
|
@@ -8882,20 +8964,20 @@ async function intelligenceRoutes(app) {
|
|
|
8882
8964
|
const project = resolveProject(app.db, request.params.name);
|
|
8883
8965
|
const parsed = request.query.limit ? parseInt(request.query.limit, 10) : NaN;
|
|
8884
8966
|
const limit = Number.isNaN(parsed) ? 30 : Math.min(Math.max(parsed, 1), 100);
|
|
8885
|
-
const rows = app.db.select().from(healthSnapshots).where(
|
|
8967
|
+
const rows = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(limit).all();
|
|
8886
8968
|
return reply.send(rows.map(mapHealthRow));
|
|
8887
8969
|
});
|
|
8888
8970
|
}
|
|
8889
8971
|
|
|
8890
8972
|
// ../api-routes/src/report.ts
|
|
8891
|
-
import { and as and12, desc as desc7, eq as
|
|
8973
|
+
import { and as and12, desc as desc7, eq as eq16, gte as gte2, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
|
|
8892
8974
|
|
|
8893
8975
|
// ../api-routes/src/content.ts
|
|
8894
|
-
import
|
|
8895
|
-
import { and as and10, desc as desc6, eq as
|
|
8976
|
+
import crypto12 from "crypto";
|
|
8977
|
+
import { and as and10, desc as desc6, eq as eq14 } from "drizzle-orm";
|
|
8896
8978
|
|
|
8897
8979
|
// ../api-routes/src/content-data.ts
|
|
8898
|
-
import { and as and9, eq as
|
|
8980
|
+
import { and as and9, eq as eq13, desc as desc5, inArray as inArray6 } from "drizzle-orm";
|
|
8899
8981
|
var RECENT_RUNS_WINDOW = 5;
|
|
8900
8982
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
8901
8983
|
const projectId = project.id;
|
|
@@ -8944,7 +9026,7 @@ function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
|
8944
9026
|
};
|
|
8945
9027
|
}
|
|
8946
9028
|
function loadDomainClasses(db, projectId) {
|
|
8947
|
-
const rows = db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(
|
|
9029
|
+
const rows = db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(eq13(domainClassifications.projectId, projectId)).all();
|
|
8948
9030
|
return new Map(rows.map((r) => [normalizeDomain(r.domain), r.competitorType]));
|
|
8949
9031
|
}
|
|
8950
9032
|
function buildQueryIntentModifiers(project, locationFilter) {
|
|
@@ -9016,22 +9098,22 @@ var US_REGION_ABBREVIATIONS = {
|
|
|
9016
9098
|
wyoming: "wy"
|
|
9017
9099
|
};
|
|
9018
9100
|
function listQueries(db, projectId) {
|
|
9019
|
-
const rows = db.select({ text: queries.query }).from(queries).where(
|
|
9101
|
+
const rows = db.select({ text: queries.query }).from(queries).where(eq13(queries.projectId, projectId)).all();
|
|
9020
9102
|
return rows.map((r) => r.text);
|
|
9021
9103
|
}
|
|
9022
9104
|
function listCompetitorDomains(db, projectId) {
|
|
9023
|
-
const rows = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
9105
|
+
const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
|
|
9024
9106
|
return rows.map((r) => r.domain);
|
|
9025
9107
|
}
|
|
9026
9108
|
function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
|
|
9027
9109
|
const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
9028
9110
|
and9(
|
|
9029
|
-
|
|
9030
|
-
|
|
9111
|
+
eq13(runs.projectId, projectId),
|
|
9112
|
+
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
9031
9113
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
9032
9114
|
// snapshots; including them risks pointing latestRunId at a run with
|
|
9033
9115
|
// no usable evidence.
|
|
9034
|
-
|
|
9116
|
+
inArray6(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
9035
9117
|
// Probe runs are operator/agent test runs; they must not poison the
|
|
9036
9118
|
// recent-runs window the content engine uses to recommend actions.
|
|
9037
9119
|
notProbeRun()
|
|
@@ -9041,22 +9123,22 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
|
|
|
9041
9123
|
return filtered.slice(0, limit).map((r) => r.id);
|
|
9042
9124
|
}
|
|
9043
9125
|
function lookupRunTimestamp(db, runId) {
|
|
9044
|
-
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(
|
|
9126
|
+
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
|
|
9045
9127
|
return row?.createdAt ?? "";
|
|
9046
9128
|
}
|
|
9047
9129
|
function listGscPagesForProject(db, projectId) {
|
|
9048
|
-
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(
|
|
9130
|
+
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
9049
9131
|
return rows.map((r) => r.page);
|
|
9050
9132
|
}
|
|
9051
9133
|
function listGa4LandingPagesForProject(db, projectId) {
|
|
9052
|
-
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(
|
|
9134
|
+
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
9053
9135
|
return rows.map((r) => r.landingPage);
|
|
9054
9136
|
}
|
|
9055
9137
|
function buildGaTrafficByPage(db, projectId) {
|
|
9056
9138
|
const rows = db.select({
|
|
9057
9139
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
9058
9140
|
sessions: gaTrafficSnapshots.sessions
|
|
9059
|
-
}).from(gaTrafficSnapshots).where(
|
|
9141
|
+
}).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
9060
9142
|
const map = /* @__PURE__ */ new Map();
|
|
9061
9143
|
for (const row of rows) {
|
|
9062
9144
|
const path8 = extractPath2(row.landingPage);
|
|
@@ -9066,24 +9148,24 @@ function buildGaTrafficByPage(db, projectId) {
|
|
|
9066
9148
|
return map;
|
|
9067
9149
|
}
|
|
9068
9150
|
function sumAiReferralSessions(db, projectId) {
|
|
9069
|
-
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(
|
|
9151
|
+
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
|
|
9070
9152
|
return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
|
|
9071
9153
|
}
|
|
9072
9154
|
function buildCandidateQueries(opts) {
|
|
9073
9155
|
if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
|
|
9074
9156
|
return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
|
|
9075
9157
|
}
|
|
9076
|
-
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(
|
|
9158
|
+
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq13(queries.projectId, opts.projectId)).all();
|
|
9077
9159
|
const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
|
|
9078
9160
|
const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
|
|
9079
|
-
const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(
|
|
9161
|
+
const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray6(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
|
|
9080
9162
|
const snapshotsByQuery = /* @__PURE__ */ new Map();
|
|
9081
9163
|
for (const row of snapshotRows) {
|
|
9082
9164
|
const list = snapshotsByQuery.get(row.queryId) ?? [];
|
|
9083
9165
|
list.push(row);
|
|
9084
9166
|
snapshotsByQuery.set(row.queryId, list);
|
|
9085
9167
|
}
|
|
9086
|
-
const gscRows = opts.db.select().from(gscSearchData).where(
|
|
9168
|
+
const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
|
|
9087
9169
|
const gscByQuery = aggregateGscByQuery(gscRows);
|
|
9088
9170
|
return opts.candidateQueryStrings.map((query) => {
|
|
9089
9171
|
const queryId = queryIdByText.get(query);
|
|
@@ -9272,7 +9354,7 @@ function extractPath2(url) {
|
|
|
9272
9354
|
|
|
9273
9355
|
// ../api-routes/src/content.ts
|
|
9274
9356
|
function loadDismissedTargetRefs(db, projectId) {
|
|
9275
|
-
const rows = db.select({ targetRef: contentTargetDismissals.targetRef }).from(contentTargetDismissals).where(
|
|
9357
|
+
const rows = db.select({ targetRef: contentTargetDismissals.targetRef }).from(contentTargetDismissals).where(eq14(contentTargetDismissals.projectId, projectId)).all();
|
|
9276
9358
|
return new Set(rows.map((r) => r.targetRef));
|
|
9277
9359
|
}
|
|
9278
9360
|
function formatDismissalRow(row) {
|
|
@@ -9367,7 +9449,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9367
9449
|
});
|
|
9368
9450
|
app.get("/projects/:name/content/dismissals", async (request) => {
|
|
9369
9451
|
const project = resolveProject(app.db, request.params.name);
|
|
9370
|
-
const rows = app.db.select().from(contentTargetDismissals).where(
|
|
9452
|
+
const rows = app.db.select().from(contentTargetDismissals).where(eq14(contentTargetDismissals.projectId, project.id)).orderBy(contentTargetDismissals.dismissedAt).all();
|
|
9371
9453
|
const response = {
|
|
9372
9454
|
dismissals: rows.map(formatDismissalRow)
|
|
9373
9455
|
};
|
|
@@ -9382,7 +9464,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9382
9464
|
const { targetRef, addressedUrl, note } = parsed.data;
|
|
9383
9465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9384
9466
|
app.db.insert(contentTargetDismissals).values({
|
|
9385
|
-
id:
|
|
9467
|
+
id: crypto12.randomUUID(),
|
|
9386
9468
|
projectId: project.id,
|
|
9387
9469
|
targetRef,
|
|
9388
9470
|
addressedUrl: addressedUrl ?? null,
|
|
@@ -9397,8 +9479,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9397
9479
|
}
|
|
9398
9480
|
}).run();
|
|
9399
9481
|
const row = app.db.select().from(contentTargetDismissals).where(and10(
|
|
9400
|
-
|
|
9401
|
-
|
|
9482
|
+
eq14(contentTargetDismissals.projectId, project.id),
|
|
9483
|
+
eq14(contentTargetDismissals.targetRef, targetRef)
|
|
9402
9484
|
)).get();
|
|
9403
9485
|
if (!row) throw notFound("contentTargetDismissal", targetRef);
|
|
9404
9486
|
return formatDismissalRow(row);
|
|
@@ -9407,8 +9489,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9407
9489
|
const project = resolveProject(app.db, request.params.name);
|
|
9408
9490
|
const { targetRef } = request.params;
|
|
9409
9491
|
const result = app.db.delete(contentTargetDismissals).where(and10(
|
|
9410
|
-
|
|
9411
|
-
|
|
9492
|
+
eq14(contentTargetDismissals.projectId, project.id),
|
|
9493
|
+
eq14(contentTargetDismissals.targetRef, targetRef)
|
|
9412
9494
|
)).run();
|
|
9413
9495
|
if (result.changes === 0) {
|
|
9414
9496
|
throw notFound("contentTargetDismissal", targetRef);
|
|
@@ -9418,8 +9500,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9418
9500
|
app.get("/projects/:name/content/recommendations/:targetRef/analysis", async (request, reply) => {
|
|
9419
9501
|
const project = resolveProject(app.db, request.params.name);
|
|
9420
9502
|
const row = app.db.select().from(recommendationExplanations).where(and10(
|
|
9421
|
-
|
|
9422
|
-
|
|
9503
|
+
eq14(recommendationExplanations.projectId, project.id),
|
|
9504
|
+
eq14(recommendationExplanations.targetRef, request.params.targetRef)
|
|
9423
9505
|
)).orderBy(desc6(recommendationExplanations.generatedAt)).limit(1).get();
|
|
9424
9506
|
if (!row) {
|
|
9425
9507
|
return reply.status(404).send({ error: { code: "NOT_FOUND", message: "No cached explanation for this targetRef." } });
|
|
@@ -9446,8 +9528,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9446
9528
|
}
|
|
9447
9529
|
if (!body.forceRefresh) {
|
|
9448
9530
|
const cached = app.db.select().from(recommendationExplanations).where(and10(
|
|
9449
|
-
|
|
9450
|
-
|
|
9531
|
+
eq14(recommendationExplanations.projectId, project.id),
|
|
9532
|
+
eq14(recommendationExplanations.targetRef, targetRef)
|
|
9451
9533
|
)).orderBy(desc6(recommendationExplanations.generatedAt)).limit(1).get();
|
|
9452
9534
|
if (cached) return reply.send(formatExplanationRow(cached));
|
|
9453
9535
|
}
|
|
@@ -9461,7 +9543,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9461
9543
|
});
|
|
9462
9544
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9463
9545
|
app.db.insert(recommendationExplanations).values({
|
|
9464
|
-
id:
|
|
9546
|
+
id: crypto12.randomUUID(),
|
|
9465
9547
|
projectId: project.id,
|
|
9466
9548
|
targetRef,
|
|
9467
9549
|
promptVersion: result.promptVersion,
|
|
@@ -9485,9 +9567,9 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9485
9567
|
}
|
|
9486
9568
|
}).run();
|
|
9487
9569
|
const row = app.db.select().from(recommendationExplanations).where(and10(
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9570
|
+
eq14(recommendationExplanations.projectId, project.id),
|
|
9571
|
+
eq14(recommendationExplanations.targetRef, targetRef),
|
|
9572
|
+
eq14(recommendationExplanations.promptVersion, result.promptVersion)
|
|
9491
9573
|
)).get();
|
|
9492
9574
|
if (!row) throw notFound("recommendationExplanation", targetRef);
|
|
9493
9575
|
return reply.send(formatExplanationRow(row));
|
|
@@ -9545,7 +9627,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9545
9627
|
});
|
|
9546
9628
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9547
9629
|
app.db.insert(recommendationBriefs).values({
|
|
9548
|
-
id:
|
|
9630
|
+
id: crypto12.randomUUID(),
|
|
9549
9631
|
projectId: project.id,
|
|
9550
9632
|
targetRef,
|
|
9551
9633
|
promptVersion: result.promptVersion,
|
|
@@ -9569,16 +9651,16 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9569
9651
|
}
|
|
9570
9652
|
}).run();
|
|
9571
9653
|
const row = app.db.select().from(recommendationBriefs).where(and10(
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9654
|
+
eq14(recommendationBriefs.projectId, project.id),
|
|
9655
|
+
eq14(recommendationBriefs.targetRef, targetRef),
|
|
9656
|
+
eq14(recommendationBriefs.promptVersion, result.promptVersion)
|
|
9575
9657
|
)).get();
|
|
9576
9658
|
if (!row) throw notFound("recommendationBrief", targetRef);
|
|
9577
9659
|
return reply.send(formatBriefRow(row));
|
|
9578
9660
|
});
|
|
9579
9661
|
app.get("/projects/:name/content/domain-classifications", async (request) => {
|
|
9580
9662
|
const project = resolveProject(app.db, request.params.name);
|
|
9581
|
-
const rows = app.db.select().from(domainClassifications).where(
|
|
9663
|
+
const rows = app.db.select().from(domainClassifications).where(eq14(domainClassifications.projectId, project.id)).orderBy(desc6(domainClassifications.hits)).all();
|
|
9582
9664
|
const response = {
|
|
9583
9665
|
classifications: rows.map((r) => ({
|
|
9584
9666
|
domain: r.domain,
|
|
@@ -9592,11 +9674,11 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9592
9674
|
}
|
|
9593
9675
|
function lookupCachedBrief(db, projectId, targetRef, promptVersion) {
|
|
9594
9676
|
const conditions = [
|
|
9595
|
-
|
|
9596
|
-
|
|
9677
|
+
eq14(recommendationBriefs.projectId, projectId),
|
|
9678
|
+
eq14(recommendationBriefs.targetRef, targetRef)
|
|
9597
9679
|
];
|
|
9598
9680
|
if (promptVersion !== void 0) {
|
|
9599
|
-
conditions.push(
|
|
9681
|
+
conditions.push(eq14(recommendationBriefs.promptVersion, promptVersion));
|
|
9600
9682
|
}
|
|
9601
9683
|
return db.select().from(recommendationBriefs).where(and10(...conditions)).orderBy(desc6(recommendationBriefs.generatedAt)).limit(1).get();
|
|
9602
9684
|
}
|
|
@@ -9624,7 +9706,7 @@ function winnabilityClassRank(winnabilityClass) {
|
|
|
9624
9706
|
}
|
|
9625
9707
|
|
|
9626
9708
|
// ../api-routes/src/gsc-totals.ts
|
|
9627
|
-
import { and as and11, asc as asc2, eq as
|
|
9709
|
+
import { and as and11, asc as asc2, eq as eq15, sql as sql6 } from "drizzle-orm";
|
|
9628
9710
|
function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
9629
9711
|
const rows = db.select({
|
|
9630
9712
|
date: gscDailyTotals.date,
|
|
@@ -9633,7 +9715,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
|
9633
9715
|
position: gscDailyTotals.position
|
|
9634
9716
|
}).from(gscDailyTotals).where(
|
|
9635
9717
|
and11(
|
|
9636
|
-
|
|
9718
|
+
eq15(gscDailyTotals.projectId, projectId),
|
|
9637
9719
|
sql6`${gscDailyTotals.date} >= ${startDate}`,
|
|
9638
9720
|
sql6`${gscDailyTotals.date} <= ${endDate}`
|
|
9639
9721
|
)
|
|
@@ -11953,7 +12035,7 @@ function loadSnapshotsForRun(db, runId) {
|
|
|
11953
12035
|
}
|
|
11954
12036
|
function loadSnapshotsForRunIds(db, runIds) {
|
|
11955
12037
|
if (runIds.length === 0) return [];
|
|
11956
|
-
const rows = db.select().from(querySnapshots).where(
|
|
12038
|
+
const rows = db.select().from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
|
|
11957
12039
|
return rows.filter((r) => r.queryId !== null).map((r) => ({
|
|
11958
12040
|
id: r.id,
|
|
11959
12041
|
runId: r.runId,
|
|
@@ -11970,13 +12052,13 @@ function loadSnapshotsForRunIds(db, runIds) {
|
|
|
11970
12052
|
}));
|
|
11971
12053
|
}
|
|
11972
12054
|
function loadQueryLookup(db, projectId) {
|
|
11973
|
-
const rows = db.select().from(queries).where(
|
|
12055
|
+
const rows = db.select().from(queries).where(eq16(queries.projectId, projectId)).all();
|
|
11974
12056
|
const byId = /* @__PURE__ */ new Map();
|
|
11975
12057
|
for (const row of rows) byId.set(row.id, row.query);
|
|
11976
12058
|
return { byId };
|
|
11977
12059
|
}
|
|
11978
12060
|
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
|
|
11979
|
-
const allRows = db.select().from(gscSearchData).where(
|
|
12061
|
+
const allRows = db.select().from(gscSearchData).where(eq16(gscSearchData.projectId, projectId)).all();
|
|
11980
12062
|
const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
|
|
11981
12063
|
let maxDate = "";
|
|
11982
12064
|
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
@@ -12068,12 +12150,12 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12068
12150
|
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
|
|
12069
12151
|
const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
|
|
12070
12152
|
and12(
|
|
12071
|
-
|
|
12072
|
-
|
|
12153
|
+
eq16(gaTrafficWindowSummaries.projectId, projectId),
|
|
12154
|
+
eq16(gaTrafficWindowSummaries.windowKey, gaWindowKey)
|
|
12073
12155
|
)
|
|
12074
12156
|
).limit(1).get() : void 0;
|
|
12075
|
-
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(
|
|
12076
|
-
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(
|
|
12157
|
+
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
12158
|
+
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, projectId)).all();
|
|
12077
12159
|
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
12078
12160
|
let snapshotMaxDate = "";
|
|
12079
12161
|
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
@@ -12100,8 +12182,8 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12100
12182
|
organicSessions: data.organic
|
|
12101
12183
|
})).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_LANDING_PAGES_LIMIT);
|
|
12102
12184
|
const aiConditions = [
|
|
12103
|
-
|
|
12104
|
-
|
|
12185
|
+
eq16(gaAiReferrals.projectId, projectId),
|
|
12186
|
+
eq16(gaAiReferrals.sourceDimension, "session")
|
|
12105
12187
|
];
|
|
12106
12188
|
if (snapshotStartDate && snapshotMaxDate) {
|
|
12107
12189
|
aiConditions.push(gte2(gaAiReferrals.date, snapshotStartDate));
|
|
@@ -12158,7 +12240,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12158
12240
|
};
|
|
12159
12241
|
}
|
|
12160
12242
|
function buildSocialReferrals(db, projectId) {
|
|
12161
|
-
const rows = db.select().from(gaSocialReferrals).where(
|
|
12243
|
+
const rows = db.select().from(gaSocialReferrals).where(eq16(gaSocialReferrals.projectId, projectId)).all();
|
|
12162
12244
|
if (rows.length === 0) return null;
|
|
12163
12245
|
let total = 0;
|
|
12164
12246
|
let organic = 0;
|
|
@@ -12190,7 +12272,7 @@ function buildSocialReferrals(db, projectId) {
|
|
|
12190
12272
|
};
|
|
12191
12273
|
}
|
|
12192
12274
|
function buildAiReferrals(db, projectId) {
|
|
12193
|
-
const rows = db.select().from(gaAiReferrals).where(
|
|
12275
|
+
const rows = db.select().from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, projectId)).all();
|
|
12194
12276
|
if (rows.length === 0) return null;
|
|
12195
12277
|
const dimSessionsByTuple = /* @__PURE__ */ new Map();
|
|
12196
12278
|
for (const r of rows) {
|
|
@@ -12303,7 +12385,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
12303
12385
|
function buildServerActivity(db, projectId, windowDays) {
|
|
12304
12386
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
12305
12387
|
and12(
|
|
12306
|
-
|
|
12388
|
+
eq16(trafficSources.projectId, projectId),
|
|
12307
12389
|
ne2(trafficSources.status, TrafficSourceStatuses.archived)
|
|
12308
12390
|
)
|
|
12309
12391
|
).all();
|
|
@@ -12319,8 +12401,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12319
12401
|
const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
12320
12402
|
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
12321
12403
|
and12(
|
|
12322
|
-
|
|
12323
|
-
|
|
12404
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12405
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12324
12406
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12325
12407
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
12326
12408
|
)
|
|
@@ -12329,7 +12411,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12329
12411
|
const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
12330
12412
|
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
12331
12413
|
and12(
|
|
12332
|
-
|
|
12414
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12333
12415
|
ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12334
12416
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12335
12417
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
@@ -12343,7 +12425,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12343
12425
|
organic: sql7`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
|
|
12344
12426
|
}).from(aiReferralEventsHourly).where(
|
|
12345
12427
|
and12(
|
|
12346
|
-
|
|
12428
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12347
12429
|
nonSubresourceReferralPathCondition(),
|
|
12348
12430
|
gte2(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
12349
12431
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
@@ -12354,7 +12436,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12354
12436
|
const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
12355
12437
|
db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
12356
12438
|
and12(
|
|
12357
|
-
|
|
12439
|
+
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12358
12440
|
gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
12359
12441
|
exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
12360
12442
|
)
|
|
@@ -12374,7 +12456,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12374
12456
|
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
12375
12457
|
}).from(crawlerEventsHourly).where(
|
|
12376
12458
|
and12(
|
|
12377
|
-
|
|
12459
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12378
12460
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
12379
12461
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12380
12462
|
)
|
|
@@ -12384,8 +12466,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12384
12466
|
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
12385
12467
|
}).from(crawlerEventsHourly).where(
|
|
12386
12468
|
and12(
|
|
12387
|
-
|
|
12388
|
-
|
|
12469
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12470
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12389
12471
|
gte2(crawlerEventsHourly.tsHour, priorStart),
|
|
12390
12472
|
lt(crawlerEventsHourly.tsHour, headlineStart)
|
|
12391
12473
|
)
|
|
@@ -12395,7 +12477,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12395
12477
|
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
12396
12478
|
}).from(aiReferralEventsHourly).where(
|
|
12397
12479
|
and12(
|
|
12398
|
-
|
|
12480
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12399
12481
|
nonSubresourceReferralPathCondition(),
|
|
12400
12482
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12401
12483
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12406,7 +12488,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12406
12488
|
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
12407
12489
|
}).from(aiUserFetchEventsHourly).where(
|
|
12408
12490
|
and12(
|
|
12409
|
-
|
|
12491
|
+
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12410
12492
|
gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
|
|
12411
12493
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12412
12494
|
)
|
|
@@ -12450,8 +12532,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12450
12532
|
operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
|
|
12451
12533
|
}).from(crawlerEventsHourly).where(
|
|
12452
12534
|
and12(
|
|
12453
|
-
|
|
12454
|
-
|
|
12535
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12536
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12455
12537
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
12456
12538
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12457
12539
|
)
|
|
@@ -12467,7 +12549,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12467
12549
|
landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
|
|
12468
12550
|
}).from(aiReferralEventsHourly).where(
|
|
12469
12551
|
and12(
|
|
12470
|
-
|
|
12552
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12471
12553
|
nonSubresourceReferralPathCondition(),
|
|
12472
12554
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12473
12555
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12484,7 +12566,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12484
12566
|
products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
|
|
12485
12567
|
}).from(aiReferralEventsHourly).where(
|
|
12486
12568
|
and12(
|
|
12487
|
-
|
|
12569
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12488
12570
|
nonSubresourceReferralPathCondition(),
|
|
12489
12571
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12490
12572
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12500,8 +12582,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12500
12582
|
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
12501
12583
|
}).from(crawlerEventsHourly).where(
|
|
12502
12584
|
and12(
|
|
12503
|
-
|
|
12504
|
-
|
|
12585
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12586
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12505
12587
|
gte2(crawlerEventsHourly.tsHour, trendStart),
|
|
12506
12588
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12507
12589
|
)
|
|
@@ -12511,7 +12593,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12511
12593
|
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
12512
12594
|
}).from(aiReferralEventsHourly).where(
|
|
12513
12595
|
and12(
|
|
12514
|
-
|
|
12596
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12515
12597
|
nonSubresourceReferralPathCondition(),
|
|
12516
12598
|
gte2(aiReferralEventsHourly.tsHour, trendStart),
|
|
12517
12599
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12522,7 +12604,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12522
12604
|
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
12523
12605
|
}).from(aiUserFetchEventsHourly).where(
|
|
12524
12606
|
and12(
|
|
12525
|
-
|
|
12607
|
+
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12526
12608
|
gte2(aiUserFetchEventsHourly.tsHour, trendStart),
|
|
12527
12609
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12528
12610
|
)
|
|
@@ -12595,7 +12677,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12595
12677
|
};
|
|
12596
12678
|
}
|
|
12597
12679
|
function buildIndexingHealth(db, projectId) {
|
|
12598
|
-
const gsc = db.select().from(gscCoverageSnapshots).where(
|
|
12680
|
+
const gsc = db.select().from(gscCoverageSnapshots).where(eq16(gscCoverageSnapshots.projectId, projectId)).orderBy(desc7(gscCoverageSnapshots.date)).limit(1).get();
|
|
12599
12681
|
if (gsc) {
|
|
12600
12682
|
const total = gsc.indexed + gsc.notIndexed;
|
|
12601
12683
|
return {
|
|
@@ -12608,7 +12690,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
12608
12690
|
indexedPct: total > 0 ? Math.round(gsc.indexed / total * 100) : 0
|
|
12609
12691
|
};
|
|
12610
12692
|
}
|
|
12611
|
-
const bing = db.select().from(bingCoverageSnapshots).where(
|
|
12693
|
+
const bing = db.select().from(bingCoverageSnapshots).where(eq16(bingCoverageSnapshots.projectId, projectId)).orderBy(desc7(bingCoverageSnapshots.date)).limit(1).get();
|
|
12612
12694
|
if (bing) {
|
|
12613
12695
|
const total = bing.indexed + bing.notIndexed + bing.unknown;
|
|
12614
12696
|
return {
|
|
@@ -12624,7 +12706,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
12624
12706
|
return null;
|
|
12625
12707
|
}
|
|
12626
12708
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
12627
|
-
const visibilityRuns = db.select().from(runs).where(and12(
|
|
12709
|
+
const visibilityRuns = db.select().from(runs).where(and12(eq16(runs.projectId, projectId), eq16(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
12628
12710
|
const totalQueries = queryLookup.byId.size;
|
|
12629
12711
|
const points = [];
|
|
12630
12712
|
for (const run of visibilityRuns) {
|
|
@@ -12673,14 +12755,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
12673
12755
|
function buildInsightList(db, projectId, locationFilter) {
|
|
12674
12756
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
12675
12757
|
and12(
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
or3(
|
|
12758
|
+
eq16(runs.projectId, projectId),
|
|
12759
|
+
eq16(runs.kind, RunKinds["answer-visibility"]),
|
|
12760
|
+
or3(eq16(runs.status, RunStatuses.completed), eq16(runs.status, RunStatuses.partial)),
|
|
12679
12761
|
notProbeRun()
|
|
12680
12762
|
)
|
|
12681
12763
|
).orderBy(desc7(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
12682
12764
|
if (recentRunIds.length === 0) return [];
|
|
12683
|
-
const rows = db.select().from(insights).where(and12(
|
|
12765
|
+
const rows = db.select().from(insights).where(and12(eq16(insights.projectId, projectId), inArray7(insights.runId, recentRunIds))).orderBy(desc7(insights.createdAt)).all();
|
|
12684
12766
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
12685
12767
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
12686
12768
|
const recommendation = r.recommendation;
|
|
@@ -13232,7 +13314,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
13232
13314
|
const project = resolveProject(db, projectName);
|
|
13233
13315
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
13234
13316
|
const comparisonWindowDays = reportComparisonWindowDays(periodDays);
|
|
13235
|
-
const allRuns = db.select().from(runs).where(and12(
|
|
13317
|
+
const allRuns = db.select().from(runs).where(and12(eq16(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
|
|
13236
13318
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
13237
13319
|
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
13238
13320
|
visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
|
|
@@ -13241,7 +13323,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
13241
13323
|
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
|
|
13242
13324
|
const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
|
|
13243
13325
|
const latestRunLocation = representativeLatestRun?.location ?? null;
|
|
13244
|
-
const competitorRows = db.select().from(competitors).where(
|
|
13326
|
+
const competitorRows = db.select().from(competitors).where(eq16(competitors.projectId, project.id)).all();
|
|
13245
13327
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
13246
13328
|
const ownedDomains = project.ownedDomains;
|
|
13247
13329
|
const projectDomains = [project.canonicalDomain, ...ownedDomains];
|
|
@@ -13468,16 +13550,16 @@ async function reportRoutes(app) {
|
|
|
13468
13550
|
}
|
|
13469
13551
|
|
|
13470
13552
|
// ../api-routes/src/citations.ts
|
|
13471
|
-
import { and as and13, eq as
|
|
13553
|
+
import { and as and13, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
13472
13554
|
async function citationRoutes(app) {
|
|
13473
13555
|
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
13474
13556
|
const project = resolveProject(app.db, request.params.name);
|
|
13475
13557
|
const configuredProviders = project.providers;
|
|
13476
|
-
const projectQueries = app.db.select().from(queries).where(
|
|
13558
|
+
const projectQueries = app.db.select().from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
13477
13559
|
if (projectQueries.length === 0) {
|
|
13478
13560
|
return reply.send(emptyCitationVisibility("no-queries"));
|
|
13479
13561
|
}
|
|
13480
|
-
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and13(
|
|
13562
|
+
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and13(eq17(runs.projectId, project.id), notProbeRun())).all();
|
|
13481
13563
|
if (projectRuns.length === 0) {
|
|
13482
13564
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
13483
13565
|
}
|
|
@@ -13492,7 +13574,7 @@ async function citationRoutes(app) {
|
|
|
13492
13574
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
13493
13575
|
answerMentioned: querySnapshots.answerMentioned,
|
|
13494
13576
|
createdAt: querySnapshots.createdAt
|
|
13495
|
-
}).from(querySnapshots).where(
|
|
13577
|
+
}).from(querySnapshots).where(inArray8(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
|
|
13496
13578
|
if (rawSnapshots.length === 0) {
|
|
13497
13579
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
13498
13580
|
}
|
|
@@ -13501,7 +13583,7 @@ async function citationRoutes(app) {
|
|
|
13501
13583
|
queryId: s.queryId,
|
|
13502
13584
|
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
13503
13585
|
}));
|
|
13504
|
-
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
13586
|
+
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
|
|
13505
13587
|
const response = computeCitationVisibility({
|
|
13506
13588
|
queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
|
|
13507
13589
|
snapshots,
|
|
@@ -13633,7 +13715,7 @@ function normalizeDomain2(domain) {
|
|
|
13633
13715
|
}
|
|
13634
13716
|
|
|
13635
13717
|
// ../api-routes/src/visibility-stats.ts
|
|
13636
|
-
import { and as and14, desc as desc8, eq as
|
|
13718
|
+
import { and as and14, desc as desc8, eq as eq18, inArray as inArray9 } from "drizzle-orm";
|
|
13637
13719
|
|
|
13638
13720
|
// ../api-routes/src/visibility-compare.ts
|
|
13639
13721
|
var VISIBILITY_COMPARE_MIN_RUNS = 5;
|
|
@@ -14031,8 +14113,8 @@ async function visibilityStatsRoutes(app) {
|
|
|
14031
14113
|
if (!Number.isInteger(n) || n <= 0) throw validationError('"lastRuns" must be a positive integer');
|
|
14032
14114
|
lastRuns = n;
|
|
14033
14115
|
}
|
|
14034
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
14035
|
-
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(
|
|
14116
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq18(queries.projectId, project.id)).all();
|
|
14117
|
+
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(eq18(runs.projectId, project.id), eq18(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
|
|
14036
14118
|
if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
|
|
14037
14119
|
if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
|
|
14038
14120
|
if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
|
|
@@ -14045,12 +14127,12 @@ async function visibilityStatsRoutes(app) {
|
|
|
14045
14127
|
citationState: querySnapshots.citationState,
|
|
14046
14128
|
answerMentioned: querySnapshots.answerMentioned,
|
|
14047
14129
|
createdAt: querySnapshots.createdAt
|
|
14048
|
-
}).from(querySnapshots).where(
|
|
14130
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all() : [];
|
|
14049
14131
|
const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
|
|
14050
14132
|
const queryAttribution = buildQueryAttribution(projectQueries);
|
|
14051
14133
|
let shareOfVoice;
|
|
14052
14134
|
if (wantShareOfVoice) {
|
|
14053
|
-
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
14135
|
+
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(eq18(competitors.projectId, project.id)).all();
|
|
14054
14136
|
const mentionShareCompetitors = competitorRows.map((c) => ({
|
|
14055
14137
|
domain: c.domain,
|
|
14056
14138
|
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
@@ -14060,7 +14142,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
14060
14142
|
queryText: querySnapshots.queryText,
|
|
14061
14143
|
answerMentioned: querySnapshots.answerMentioned,
|
|
14062
14144
|
answerText: querySnapshots.answerText
|
|
14063
|
-
}).from(querySnapshots).where(
|
|
14145
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all() : [];
|
|
14064
14146
|
const attributedSovSnapshots = sovSnapshots.filter((s) => resolveCurrentQuery(queryAttribution, s) !== void 0);
|
|
14065
14147
|
const result = buildMentionShare(
|
|
14066
14148
|
attributedSovSnapshots.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
|
|
@@ -14119,8 +14201,8 @@ async function visibilityStatsRoutes(app) {
|
|
|
14119
14201
|
if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
|
|
14120
14202
|
throw validationError('"from" must be a month strictly before "to"');
|
|
14121
14203
|
}
|
|
14122
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
14123
|
-
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
14204
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq18(queries.projectId, project.id)).all();
|
|
14205
|
+
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(eq18(competitors.projectId, project.id)).all();
|
|
14124
14206
|
const competitorInputs = competitorRows.map((c) => ({
|
|
14125
14207
|
domain: c.domain,
|
|
14126
14208
|
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
@@ -14128,7 +14210,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
14128
14210
|
const loadMonth = (bounds) => {
|
|
14129
14211
|
const sinceMs = Date.parse(bounds.since);
|
|
14130
14212
|
const untilMs = Date.parse(bounds.until);
|
|
14131
|
-
const monthRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(
|
|
14213
|
+
const monthRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(eq18(runs.projectId, project.id), eq18(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter(
|
|
14132
14214
|
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
14133
14215
|
);
|
|
14134
14216
|
const runIds = monthRuns.map((r) => r.id);
|
|
@@ -14141,7 +14223,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
14141
14223
|
answerMentioned: querySnapshots.answerMentioned,
|
|
14142
14224
|
answerText: querySnapshots.answerText,
|
|
14143
14225
|
citedDomains: querySnapshots.citedDomains
|
|
14144
|
-
}).from(querySnapshots).where(
|
|
14226
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all() : [];
|
|
14145
14227
|
return { runCount: monthRuns.length, snapshots };
|
|
14146
14228
|
};
|
|
14147
14229
|
const fromMonth = loadMonth(fromBounds);
|
|
@@ -14158,7 +14240,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
14158
14240
|
}
|
|
14159
14241
|
|
|
14160
14242
|
// ../api-routes/src/composites.ts
|
|
14161
|
-
import { eq as
|
|
14243
|
+
import { eq as eq19, and as and15, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
|
|
14162
14244
|
var TOP_INSIGHT_LIMIT = 5;
|
|
14163
14245
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
14164
14246
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -14176,7 +14258,7 @@ async function compositeRoutes(app) {
|
|
|
14176
14258
|
const project = resolveProject(app.db, request.params.name);
|
|
14177
14259
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
14178
14260
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
14179
|
-
const allRunsRaw = app.db.select().from(runs).where(and15(
|
|
14261
|
+
const allRunsRaw = app.db.select().from(runs).where(and15(eq19(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
14180
14262
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
14181
14263
|
const totalRuns = allRuns.length;
|
|
14182
14264
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -14189,15 +14271,15 @@ async function compositeRoutes(app) {
|
|
|
14189
14271
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
14190
14272
|
const latestRunRow = allRuns[0] ?? null;
|
|
14191
14273
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
14192
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
14274
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq19(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
14193
14275
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
14194
|
-
const insightRows = app.db.select().from(insights).where(
|
|
14276
|
+
const insightRows = app.db.select().from(insights).where(eq19(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
14195
14277
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
14196
14278
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
14197
14279
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
14198
14280
|
for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
|
|
14199
14281
|
for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
|
|
14200
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
14282
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq19(queries.projectId, project.id)).all();
|
|
14201
14283
|
const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
|
|
14202
14284
|
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
|
|
14203
14285
|
const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
@@ -14213,7 +14295,7 @@ async function compositeRoutes(app) {
|
|
|
14213
14295
|
trackedPrevious,
|
|
14214
14296
|
previousVisibilityRun?.createdAt ?? null
|
|
14215
14297
|
);
|
|
14216
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
14298
|
+
const competitorRows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
|
|
14217
14299
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
14218
14300
|
for (const snapshots of snapshotsByRun.values()) {
|
|
14219
14301
|
for (const snapshot of snapshots) {
|
|
@@ -14331,9 +14413,9 @@ async function compositeRoutes(app) {
|
|
|
14331
14413
|
citedDomains: querySnapshots.citedDomains,
|
|
14332
14414
|
rawResponse: querySnapshots.rawResponse,
|
|
14333
14415
|
createdAt: querySnapshots.createdAt
|
|
14334
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
14416
|
+
}).from(querySnapshots).innerJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(
|
|
14335
14417
|
and15(
|
|
14336
|
-
|
|
14418
|
+
eq19(queries.projectId, project.id),
|
|
14337
14419
|
or4(
|
|
14338
14420
|
sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
14339
14421
|
sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -14344,7 +14426,7 @@ async function compositeRoutes(app) {
|
|
|
14344
14426
|
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
14345
14427
|
const insightMatches = app.db.select().from(insights).where(
|
|
14346
14428
|
and15(
|
|
14347
|
-
|
|
14429
|
+
eq19(insights.projectId, project.id),
|
|
14348
14430
|
or4(
|
|
14349
14431
|
like2(insights.title, pattern),
|
|
14350
14432
|
like2(insights.query, pattern),
|
|
@@ -14423,7 +14505,7 @@ function loadSnapshotsByRunIds(app, runIds, queryIdByText) {
|
|
|
14423
14505
|
answerText: querySnapshots.answerText,
|
|
14424
14506
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
14425
14507
|
citedDomains: querySnapshots.citedDomains
|
|
14426
|
-
}).from(querySnapshots).where(
|
|
14508
|
+
}).from(querySnapshots).where(inArray10(querySnapshots.runId, [...runIds])).all();
|
|
14427
14509
|
for (const row of rows) {
|
|
14428
14510
|
const queryText = row.queryText?.trim() || null;
|
|
14429
14511
|
let queryId;
|
|
@@ -14561,7 +14643,7 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
14561
14643
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
14562
14644
|
avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
14563
14645
|
}).from(gscSearchData).where(and15(
|
|
14564
|
-
|
|
14646
|
+
eq19(gscSearchData.projectId, projectId),
|
|
14565
14647
|
sql8`${gscSearchData.date} >= ${cutoff}`,
|
|
14566
14648
|
sql8`${gscSearchData.impressions} > 0`
|
|
14567
14649
|
)).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
@@ -14584,8 +14666,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
14584
14666
|
tooltip,
|
|
14585
14667
|
trend: []
|
|
14586
14668
|
};
|
|
14587
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
14588
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
14669
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq19(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
14670
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq19(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
14589
14671
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
14590
14672
|
if (!chosen) return empty;
|
|
14591
14673
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -14611,7 +14693,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
14611
14693
|
url: gscUrlInspections.url,
|
|
14612
14694
|
indexingState: gscUrlInspections.indexingState,
|
|
14613
14695
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
14614
|
-
}).from(gscUrlInspections).where(
|
|
14696
|
+
}).from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
14615
14697
|
if (rows.length === 0) return 0;
|
|
14616
14698
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
14617
14699
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -19349,8 +19431,8 @@ async function settingsRoutes(app, opts) {
|
|
|
19349
19431
|
}
|
|
19350
19432
|
|
|
19351
19433
|
// ../api-routes/src/keys.ts
|
|
19352
|
-
import
|
|
19353
|
-
import { desc as desc10, eq as
|
|
19434
|
+
import crypto13 from "crypto";
|
|
19435
|
+
import { desc as desc10, eq as eq20 } from "drizzle-orm";
|
|
19354
19436
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
19355
19437
|
function toApiKeyDto(row) {
|
|
19356
19438
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -19369,7 +19451,7 @@ function toApiKeyDto(row) {
|
|
|
19369
19451
|
async function keysRoutes(app) {
|
|
19370
19452
|
app.get("/keys", async (request) => {
|
|
19371
19453
|
const scopedProjectId = request.apiKey?.projectId;
|
|
19372
|
-
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(
|
|
19454
|
+
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq20(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
19373
19455
|
return { keys: rows.map(toApiKeyDto) };
|
|
19374
19456
|
});
|
|
19375
19457
|
app.get("/keys/self", async (request) => {
|
|
@@ -19377,7 +19459,7 @@ async function keysRoutes(app) {
|
|
|
19377
19459
|
if (!id) {
|
|
19378
19460
|
throw notFound("API key", "self");
|
|
19379
19461
|
}
|
|
19380
|
-
const row = app.db.select().from(apiKeys).where(
|
|
19462
|
+
const row = app.db.select().from(apiKeys).where(eq20(apiKeys.id, id)).get();
|
|
19381
19463
|
if (!row) {
|
|
19382
19464
|
throw notFound("API key", id);
|
|
19383
19465
|
}
|
|
@@ -19395,15 +19477,15 @@ async function keysRoutes(app) {
|
|
|
19395
19477
|
throw forbidden("A project-scoped key can only mint keys scoped to its own project.");
|
|
19396
19478
|
}
|
|
19397
19479
|
if (projectId) {
|
|
19398
|
-
const proj = app.db.select({ id: projects.id }).from(projects).where(
|
|
19480
|
+
const proj = app.db.select({ id: projects.id }).from(projects).where(eq20(projects.id, projectId)).get();
|
|
19399
19481
|
if (!proj) {
|
|
19400
19482
|
throw notFound("Project", projectId);
|
|
19401
19483
|
}
|
|
19402
19484
|
}
|
|
19403
|
-
const raw = `cnry_${
|
|
19485
|
+
const raw = `cnry_${crypto13.randomBytes(16).toString("hex")}`;
|
|
19404
19486
|
const keyHash = hashApiKey(raw);
|
|
19405
19487
|
const keyPrefix = raw.slice(0, 9);
|
|
19406
|
-
const id =
|
|
19488
|
+
const id = crypto13.randomUUID();
|
|
19407
19489
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19408
19490
|
const effectiveScopes = scopes ?? ["*"];
|
|
19409
19491
|
app.db.transaction((tx) => {
|
|
@@ -19441,7 +19523,7 @@ async function keysRoutes(app) {
|
|
|
19441
19523
|
app.post("/keys/:id/revoke", async (request) => {
|
|
19442
19524
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
19443
19525
|
const { id } = request.params;
|
|
19444
|
-
const row = app.db.select().from(apiKeys).where(
|
|
19526
|
+
const row = app.db.select().from(apiKeys).where(eq20(apiKeys.id, id)).get();
|
|
19445
19527
|
if (!row) {
|
|
19446
19528
|
throw notFound("API key", id);
|
|
19447
19529
|
}
|
|
@@ -19457,7 +19539,7 @@ async function keysRoutes(app) {
|
|
|
19457
19539
|
}
|
|
19458
19540
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19459
19541
|
app.db.transaction((tx) => {
|
|
19460
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
19542
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq20(apiKeys.id, id)).run();
|
|
19461
19543
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
19462
19544
|
actor: "api",
|
|
19463
19545
|
action: "api-key.revoked",
|
|
@@ -19529,8 +19611,8 @@ async function telemetryRoutes(app, opts) {
|
|
|
19529
19611
|
}
|
|
19530
19612
|
|
|
19531
19613
|
// ../api-routes/src/schedules.ts
|
|
19532
|
-
import
|
|
19533
|
-
import { and as and16, eq as
|
|
19614
|
+
import crypto14 from "crypto";
|
|
19615
|
+
import { and as and16, eq as eq21 } from "drizzle-orm";
|
|
19534
19616
|
function parseKindParam(raw) {
|
|
19535
19617
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
19536
19618
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -19557,7 +19639,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19557
19639
|
if (!sourceId) {
|
|
19558
19640
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
19559
19641
|
}
|
|
19560
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
19642
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq21(trafficSources.id, sourceId)).get();
|
|
19561
19643
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
19562
19644
|
throw notFound("Traffic source", sourceId);
|
|
19563
19645
|
}
|
|
@@ -19602,7 +19684,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19602
19684
|
}
|
|
19603
19685
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19604
19686
|
const enabledBool = enabled !== false;
|
|
19605
|
-
const existing = app.db.select().from(schedules).where(and16(
|
|
19687
|
+
const existing = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19606
19688
|
if (existing) {
|
|
19607
19689
|
app.db.update(schedules).set({
|
|
19608
19690
|
cronExpr,
|
|
@@ -19612,10 +19694,10 @@ async function scheduleRoutes(app, opts) {
|
|
|
19612
19694
|
sourceId: sourceId ?? null,
|
|
19613
19695
|
enabled: enabledBool,
|
|
19614
19696
|
updatedAt: now
|
|
19615
|
-
}).where(
|
|
19697
|
+
}).where(eq21(schedules.id, existing.id)).run();
|
|
19616
19698
|
} else {
|
|
19617
19699
|
app.db.insert(schedules).values({
|
|
19618
|
-
id:
|
|
19700
|
+
id: crypto14.randomUUID(),
|
|
19619
19701
|
projectId: project.id,
|
|
19620
19702
|
kind,
|
|
19621
19703
|
cronExpr,
|
|
@@ -19636,13 +19718,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
19636
19718
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
19637
19719
|
});
|
|
19638
19720
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
19639
|
-
const schedule = app.db.select().from(schedules).where(and16(
|
|
19721
|
+
const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19640
19722
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
19641
19723
|
});
|
|
19642
19724
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
19643
19725
|
const project = resolveProject(app.db, request.params.name);
|
|
19644
19726
|
const kind = parseKindParam(request.query?.kind);
|
|
19645
|
-
const schedule = app.db.select().from(schedules).where(and16(
|
|
19727
|
+
const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19646
19728
|
if (!schedule) {
|
|
19647
19729
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
19648
19730
|
}
|
|
@@ -19651,11 +19733,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
19651
19733
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
19652
19734
|
const project = resolveProject(app.db, request.params.name);
|
|
19653
19735
|
const kind = parseKindParam(request.query?.kind);
|
|
19654
|
-
const schedule = app.db.select().from(schedules).where(and16(
|
|
19736
|
+
const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19655
19737
|
if (!schedule) {
|
|
19656
19738
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
19657
19739
|
}
|
|
19658
|
-
app.db.delete(schedules).where(
|
|
19740
|
+
app.db.delete(schedules).where(eq21(schedules.id, schedule.id)).run();
|
|
19659
19741
|
writeAuditLog(app.db, {
|
|
19660
19742
|
projectId: project.id,
|
|
19661
19743
|
actor: "api",
|
|
@@ -19687,8 +19769,8 @@ function formatSchedule(row) {
|
|
|
19687
19769
|
}
|
|
19688
19770
|
|
|
19689
19771
|
// ../api-routes/src/notifications.ts
|
|
19690
|
-
import
|
|
19691
|
-
import { eq as
|
|
19772
|
+
import crypto15 from "crypto";
|
|
19773
|
+
import { eq as eq22 } from "drizzle-orm";
|
|
19692
19774
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
19693
19775
|
async function notificationRoutes(app, opts = {}) {
|
|
19694
19776
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -19707,8 +19789,8 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19707
19789
|
throw validationError(`Invalid event(s): ${invalid.join(", ")}. Must be one of: ${VALID_EVENTS.join(", ")}`);
|
|
19708
19790
|
}
|
|
19709
19791
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19710
|
-
const id =
|
|
19711
|
-
const webhookSecret =
|
|
19792
|
+
const id = crypto15.randomUUID();
|
|
19793
|
+
const webhookSecret = crypto15.randomBytes(32).toString("hex");
|
|
19712
19794
|
app.db.insert(notifications).values({
|
|
19713
19795
|
id,
|
|
19714
19796
|
projectId: project.id,
|
|
@@ -19728,22 +19810,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19728
19810
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
19729
19811
|
});
|
|
19730
19812
|
return reply.status(201).send({
|
|
19731
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
19813
|
+
...formatNotification(app.db.select().from(notifications).where(eq22(notifications.id, id)).get()),
|
|
19732
19814
|
webhookSecret
|
|
19733
19815
|
});
|
|
19734
19816
|
});
|
|
19735
19817
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
19736
19818
|
const project = resolveProject(app.db, request.params.name);
|
|
19737
|
-
const rows = app.db.select().from(notifications).where(
|
|
19819
|
+
const rows = app.db.select().from(notifications).where(eq22(notifications.projectId, project.id)).all();
|
|
19738
19820
|
return reply.send(rows.map(formatNotification));
|
|
19739
19821
|
});
|
|
19740
19822
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
19741
19823
|
const project = resolveProject(app.db, request.params.name);
|
|
19742
|
-
const notification = app.db.select().from(notifications).where(
|
|
19824
|
+
const notification = app.db.select().from(notifications).where(eq22(notifications.id, request.params.id)).get();
|
|
19743
19825
|
if (!notification || notification.projectId !== project.id) {
|
|
19744
19826
|
throw notFound("Notification", request.params.id);
|
|
19745
19827
|
}
|
|
19746
|
-
app.db.delete(notifications).where(
|
|
19828
|
+
app.db.delete(notifications).where(eq22(notifications.id, notification.id)).run();
|
|
19747
19829
|
writeAuditLog(app.db, {
|
|
19748
19830
|
projectId: project.id,
|
|
19749
19831
|
actor: "api",
|
|
@@ -19755,7 +19837,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19755
19837
|
});
|
|
19756
19838
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
19757
19839
|
const project = resolveProject(app.db, request.params.name);
|
|
19758
|
-
const notification = app.db.select().from(notifications).where(
|
|
19840
|
+
const notification = app.db.select().from(notifications).where(eq22(notifications.id, request.params.id)).get();
|
|
19759
19841
|
if (!notification || notification.projectId !== project.id) {
|
|
19760
19842
|
throw notFound("Notification", request.params.id);
|
|
19761
19843
|
}
|
|
@@ -19807,8 +19889,8 @@ function formatNotification(row) {
|
|
|
19807
19889
|
}
|
|
19808
19890
|
|
|
19809
19891
|
// ../api-routes/src/google.ts
|
|
19810
|
-
import
|
|
19811
|
-
import { eq as
|
|
19892
|
+
import crypto19 from "crypto";
|
|
19893
|
+
import { eq as eq23, and as and17, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
|
|
19812
19894
|
|
|
19813
19895
|
// ../api-routes/src/gbp-summary.ts
|
|
19814
19896
|
function computeMetricTotals(rows) {
|
|
@@ -20343,7 +20425,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
|
20343
20425
|
}
|
|
20344
20426
|
|
|
20345
20427
|
// ../integration-google-analytics/src/ga4-client.ts
|
|
20346
|
-
import
|
|
20428
|
+
import crypto16 from "crypto";
|
|
20347
20429
|
|
|
20348
20430
|
// ../integration-google-analytics/src/constants.ts
|
|
20349
20431
|
var GA4_DATA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
|
|
@@ -20450,7 +20532,7 @@ function createServiceAccountJwt(clientEmail, privateKey, scope) {
|
|
|
20450
20532
|
const headerB64 = encode(header);
|
|
20451
20533
|
const payloadB64 = encode(payload);
|
|
20452
20534
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
20453
|
-
const sign =
|
|
20535
|
+
const sign = crypto16.createSign("RSA-SHA256");
|
|
20454
20536
|
sign.update(signingInput);
|
|
20455
20537
|
const signature = sign.sign(privateKey, "base64url");
|
|
20456
20538
|
return `${signingInput}.${signature}`;
|
|
@@ -21330,7 +21412,7 @@ async function listPlaceActionLinks(accessToken, locationName, opts = {}) {
|
|
|
21330
21412
|
}
|
|
21331
21413
|
|
|
21332
21414
|
// ../integration-google-business-profile/src/lodging-client.ts
|
|
21333
|
-
import
|
|
21415
|
+
import crypto17 from "crypto";
|
|
21334
21416
|
async function getLodging(accessToken, locationName, opts = {}) {
|
|
21335
21417
|
const url = `${GBP_LODGING_BASE}/${locationName}/lodging?readMask=*`;
|
|
21336
21418
|
try {
|
|
@@ -21357,7 +21439,7 @@ function isPopulated(value) {
|
|
|
21357
21439
|
return true;
|
|
21358
21440
|
}
|
|
21359
21441
|
function hashLodging(lodging) {
|
|
21360
|
-
return
|
|
21442
|
+
return crypto17.createHash("sha256").update(stableStringify2(lodging)).digest("hex");
|
|
21361
21443
|
}
|
|
21362
21444
|
function stableStringify2(value) {
|
|
21363
21445
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -21368,7 +21450,7 @@ function stableStringify2(value) {
|
|
|
21368
21450
|
}
|
|
21369
21451
|
|
|
21370
21452
|
// ../integration-google-business-profile/src/attributes-client.ts
|
|
21371
|
-
import
|
|
21453
|
+
import crypto18 from "crypto";
|
|
21372
21454
|
async function getAttributes(accessToken, locationName, opts = {}) {
|
|
21373
21455
|
const url = `${GBP_BUSINESS_INFO_BASE}/${locationName}/attributes`;
|
|
21374
21456
|
let res;
|
|
@@ -21398,7 +21480,7 @@ function countAttributes(attrs) {
|
|
|
21398
21480
|
}
|
|
21399
21481
|
function hashAttributes(attrs) {
|
|
21400
21482
|
const sorted = [...attrs].sort((a, b) => a.name.localeCompare(b.name));
|
|
21401
|
-
return
|
|
21483
|
+
return crypto18.createHash("sha256").update(stableStringify3(sorted)).digest("hex");
|
|
21402
21484
|
}
|
|
21403
21485
|
function stableStringify3(value) {
|
|
21404
21486
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -21420,7 +21502,7 @@ function scopesForConnectionType(type) {
|
|
|
21420
21502
|
}
|
|
21421
21503
|
}
|
|
21422
21504
|
function signState(payload, secret) {
|
|
21423
|
-
return
|
|
21505
|
+
return crypto19.createHmac("sha256", secret).update(payload).digest("hex");
|
|
21424
21506
|
}
|
|
21425
21507
|
function buildSignedState(data, secret) {
|
|
21426
21508
|
const payload = JSON.stringify(data);
|
|
@@ -21431,7 +21513,7 @@ function verifySignedState(encoded, secret) {
|
|
|
21431
21513
|
try {
|
|
21432
21514
|
const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
|
|
21433
21515
|
const expected = signState(payload, secret);
|
|
21434
|
-
if (!
|
|
21516
|
+
if (!crypto19.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
|
|
21435
21517
|
return JSON.parse(payload);
|
|
21436
21518
|
} catch {
|
|
21437
21519
|
return null;
|
|
@@ -21648,7 +21730,7 @@ async function googleRoutes(app, opts) {
|
|
|
21648
21730
|
if (!projectId) {
|
|
21649
21731
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
21650
21732
|
}
|
|
21651
|
-
const project = app.db.select().from(projects).where(
|
|
21733
|
+
const project = app.db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
21652
21734
|
if (!project) {
|
|
21653
21735
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
21654
21736
|
}
|
|
@@ -21767,7 +21849,7 @@ async function googleRoutes(app, opts) {
|
|
|
21767
21849
|
throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
21768
21850
|
}
|
|
21769
21851
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21770
|
-
const runId =
|
|
21852
|
+
const runId = crypto19.randomUUID();
|
|
21771
21853
|
app.db.insert(runs).values({
|
|
21772
21854
|
id: runId,
|
|
21773
21855
|
projectId: project.id,
|
|
@@ -21780,14 +21862,14 @@ async function googleRoutes(app, opts) {
|
|
|
21780
21862
|
if (opts.onGscSyncRequested) {
|
|
21781
21863
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
21782
21864
|
}
|
|
21783
|
-
const run = app.db.select().from(runs).where(
|
|
21865
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
21784
21866
|
return run;
|
|
21785
21867
|
});
|
|
21786
21868
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
21787
21869
|
const project = resolveProject(app.db, request.params.name);
|
|
21788
21870
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
21789
21871
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
21790
|
-
const conditions = [
|
|
21872
|
+
const conditions = [eq23(gscSearchData.projectId, project.id)];
|
|
21791
21873
|
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
21792
21874
|
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
21793
21875
|
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -21815,7 +21897,7 @@ async function googleRoutes(app, opts) {
|
|
|
21815
21897
|
const windowStart = startDate ?? cutoffDate ?? "";
|
|
21816
21898
|
const windowEnd = endDate ?? "9999-12-31";
|
|
21817
21899
|
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
21818
|
-
const conditions = [
|
|
21900
|
+
const conditions = [eq23(gscSearchData.projectId, project.id)];
|
|
21819
21901
|
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
21820
21902
|
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
21821
21903
|
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -21876,7 +21958,7 @@ async function googleRoutes(app, opts) {
|
|
|
21876
21958
|
const mob = ir.mobileUsabilityResult;
|
|
21877
21959
|
const rich = ir.richResultsResult;
|
|
21878
21960
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21879
|
-
const id =
|
|
21961
|
+
const id = crypto19.randomUUID();
|
|
21880
21962
|
app.db.insert(gscUrlInspections).values({
|
|
21881
21963
|
id,
|
|
21882
21964
|
projectId: project.id,
|
|
@@ -21914,8 +21996,8 @@ async function googleRoutes(app, opts) {
|
|
|
21914
21996
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
21915
21997
|
const project = resolveProject(app.db, request.params.name);
|
|
21916
21998
|
const { url, limit } = request.query;
|
|
21917
|
-
const conditions = [
|
|
21918
|
-
if (url) conditions.push(
|
|
21999
|
+
const conditions = [eq23(gscUrlInspections.projectId, project.id)];
|
|
22000
|
+
if (url) conditions.push(eq23(gscUrlInspections.url, url));
|
|
21919
22001
|
const rows = app.db.select().from(gscUrlInspections).where(and17(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
21920
22002
|
return rows.map((r) => ({
|
|
21921
22003
|
id: r.id,
|
|
@@ -21935,7 +22017,7 @@ async function googleRoutes(app, opts) {
|
|
|
21935
22017
|
});
|
|
21936
22018
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
21937
22019
|
const project = resolveProject(app.db, request.params.name);
|
|
21938
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22020
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21939
22021
|
const byUrl = /* @__PURE__ */ new Map();
|
|
21940
22022
|
for (const row of allInspections) {
|
|
21941
22023
|
const existing = byUrl.get(row.url);
|
|
@@ -21963,7 +22045,7 @@ async function googleRoutes(app, opts) {
|
|
|
21963
22045
|
});
|
|
21964
22046
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
21965
22047
|
const project = resolveProject(app.db, request.params.name);
|
|
21966
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22048
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21967
22049
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
21968
22050
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21969
22051
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -22012,7 +22094,7 @@ async function googleRoutes(app, opts) {
|
|
|
22012
22094
|
const total = latestByUrl.size;
|
|
22013
22095
|
const indexed = indexedUrls.length;
|
|
22014
22096
|
const notIndexed = notIndexedUrls.length;
|
|
22015
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
22097
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq23(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
22016
22098
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
22017
22099
|
const formatRow = (r) => ({
|
|
22018
22100
|
id: r.id,
|
|
@@ -22063,7 +22145,7 @@ async function googleRoutes(app, opts) {
|
|
|
22063
22145
|
const project = resolveProject(app.db, request.params.name);
|
|
22064
22146
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
22065
22147
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
22066
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
22148
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq23(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
22067
22149
|
return rows.map((r) => ({
|
|
22068
22150
|
date: r.date,
|
|
22069
22151
|
indexed: r.indexed,
|
|
@@ -22120,7 +22202,7 @@ async function googleRoutes(app, opts) {
|
|
|
22120
22202
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22121
22203
|
});
|
|
22122
22204
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22123
|
-
const runId =
|
|
22205
|
+
const runId = crypto19.randomUUID();
|
|
22124
22206
|
app.db.insert(runs).values({
|
|
22125
22207
|
id: runId,
|
|
22126
22208
|
projectId: project.id,
|
|
@@ -22132,7 +22214,7 @@ async function googleRoutes(app, opts) {
|
|
|
22132
22214
|
if (opts.onInspectSitemapRequested) {
|
|
22133
22215
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
22134
22216
|
}
|
|
22135
|
-
const run = app.db.select().from(runs).where(
|
|
22217
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
22136
22218
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
22137
22219
|
});
|
|
22138
22220
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -22146,7 +22228,7 @@ async function googleRoutes(app, opts) {
|
|
|
22146
22228
|
throw validationError("No GSC property configured for this connection");
|
|
22147
22229
|
}
|
|
22148
22230
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22149
|
-
const runId =
|
|
22231
|
+
const runId = crypto19.randomUUID();
|
|
22150
22232
|
app.db.insert(runs).values({
|
|
22151
22233
|
id: runId,
|
|
22152
22234
|
projectId: project.id,
|
|
@@ -22159,7 +22241,7 @@ async function googleRoutes(app, opts) {
|
|
|
22159
22241
|
if (opts.onInspectSitemapRequested) {
|
|
22160
22242
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
22161
22243
|
}
|
|
22162
|
-
const run = app.db.select().from(runs).where(
|
|
22244
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
22163
22245
|
return run;
|
|
22164
22246
|
});
|
|
22165
22247
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -22206,7 +22288,7 @@ async function googleRoutes(app, opts) {
|
|
|
22206
22288
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
22207
22289
|
let urlsToNotify = request.body?.urls ?? [];
|
|
22208
22290
|
if (request.body?.allUnindexed) {
|
|
22209
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22291
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
22210
22292
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22211
22293
|
for (const row of allInspections) {
|
|
22212
22294
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -22324,7 +22406,7 @@ async function googleRoutes(app, opts) {
|
|
|
22324
22406
|
};
|
|
22325
22407
|
}
|
|
22326
22408
|
function listSelectionResponse(projectId) {
|
|
22327
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
22409
|
+
const rows = app.db.select().from(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).all();
|
|
22328
22410
|
const dtos = rows.map(rowToDto2);
|
|
22329
22411
|
return {
|
|
22330
22412
|
locations: dtos,
|
|
@@ -22333,15 +22415,15 @@ async function googleRoutes(app, opts) {
|
|
|
22333
22415
|
};
|
|
22334
22416
|
}
|
|
22335
22417
|
function clearGbpProjectData(tx, projectId) {
|
|
22336
|
-
tx.delete(gbpDailyMetrics).where(
|
|
22337
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
22338
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
22339
|
-
tx.delete(gbpPlaceActions).where(
|
|
22340
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
22341
|
-
tx.delete(gbpLocations).where(
|
|
22418
|
+
tx.delete(gbpDailyMetrics).where(eq23(gbpDailyMetrics.projectId, projectId)).run();
|
|
22419
|
+
tx.delete(gbpKeywordImpressions).where(eq23(gbpKeywordImpressions.projectId, projectId)).run();
|
|
22420
|
+
tx.delete(gbpKeywordMonthly).where(eq23(gbpKeywordMonthly.projectId, projectId)).run();
|
|
22421
|
+
tx.delete(gbpPlaceActions).where(eq23(gbpPlaceActions.projectId, projectId)).run();
|
|
22422
|
+
tx.delete(gbpLodgingSnapshots).where(eq23(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
22423
|
+
tx.delete(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).run();
|
|
22342
22424
|
}
|
|
22343
22425
|
function currentProjectAccount(projectId) {
|
|
22344
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
22426
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).limit(1).get();
|
|
22345
22427
|
return row?.accountName ?? null;
|
|
22346
22428
|
}
|
|
22347
22429
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -22411,7 +22493,7 @@ async function googleRoutes(app, opts) {
|
|
|
22411
22493
|
app.db.transaction((tx) => {
|
|
22412
22494
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
22413
22495
|
for (const remote of remoteLocations) {
|
|
22414
|
-
const existing = tx.select().from(gbpLocations).where(and17(
|
|
22496
|
+
const existing = tx.select().from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.locationName, remote.name))).get();
|
|
22415
22497
|
const profile = buildLocationProfileFields(remote);
|
|
22416
22498
|
if (existing) {
|
|
22417
22499
|
tx.update(gbpLocations).set({
|
|
@@ -22424,10 +22506,10 @@ async function googleRoutes(app, opts) {
|
|
|
22424
22506
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
22425
22507
|
...profile,
|
|
22426
22508
|
updatedAt: now
|
|
22427
|
-
}).where(
|
|
22509
|
+
}).where(eq23(gbpLocations.id, existing.id)).run();
|
|
22428
22510
|
} else {
|
|
22429
22511
|
tx.insert(gbpLocations).values({
|
|
22430
|
-
id:
|
|
22512
|
+
id: crypto19.randomUUID(),
|
|
22431
22513
|
projectId: project.id,
|
|
22432
22514
|
accountName,
|
|
22433
22515
|
locationName: remote.name,
|
|
@@ -22508,11 +22590,11 @@ async function googleRoutes(app, opts) {
|
|
|
22508
22590
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
22509
22591
|
}
|
|
22510
22592
|
const { selected } = parsed.data;
|
|
22511
|
-
const existing = app.db.select().from(gbpLocations).where(and17(
|
|
22593
|
+
const existing = app.db.select().from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.locationName, locationName))).get();
|
|
22512
22594
|
if (!existing) throw notFound("GBP location", locationName);
|
|
22513
22595
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22514
22596
|
app.db.transaction((tx) => {
|
|
22515
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
22597
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq23(gbpLocations.id, existing.id)).run();
|
|
22516
22598
|
writeAuditLog(tx, {
|
|
22517
22599
|
projectId: project.id,
|
|
22518
22600
|
actor: "api",
|
|
@@ -22521,7 +22603,7 @@ async function googleRoutes(app, opts) {
|
|
|
22521
22603
|
entityId: locationName
|
|
22522
22604
|
});
|
|
22523
22605
|
});
|
|
22524
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
22606
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq23(gbpLocations.id, existing.id)).get();
|
|
22525
22607
|
return rowToDto2(refreshed);
|
|
22526
22608
|
});
|
|
22527
22609
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -22551,7 +22633,7 @@ async function googleRoutes(app, opts) {
|
|
|
22551
22633
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
|
|
22552
22634
|
}
|
|
22553
22635
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22554
|
-
const runId =
|
|
22636
|
+
const runId = crypto19.randomUUID();
|
|
22555
22637
|
app.db.insert(runs).values({
|
|
22556
22638
|
id: runId,
|
|
22557
22639
|
projectId: project.id,
|
|
@@ -22565,9 +22647,9 @@ async function googleRoutes(app, opts) {
|
|
|
22565
22647
|
});
|
|
22566
22648
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
22567
22649
|
const project = resolveProject(app.db, request.params.name);
|
|
22568
|
-
const conditions = [
|
|
22569
|
-
if (request.query.locationName) conditions.push(
|
|
22570
|
-
if (request.query.metric) conditions.push(
|
|
22650
|
+
const conditions = [eq23(gbpDailyMetrics.projectId, project.id)];
|
|
22651
|
+
if (request.query.locationName) conditions.push(eq23(gbpDailyMetrics.locationName, request.query.locationName));
|
|
22652
|
+
if (request.query.metric) conditions.push(eq23(gbpDailyMetrics.metric, request.query.metric));
|
|
22571
22653
|
const rows = app.db.select().from(gbpDailyMetrics).where(and17(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
22572
22654
|
return {
|
|
22573
22655
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
@@ -22576,8 +22658,8 @@ async function googleRoutes(app, opts) {
|
|
|
22576
22658
|
});
|
|
22577
22659
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
22578
22660
|
const project = resolveProject(app.db, request.params.name);
|
|
22579
|
-
const conditions = [
|
|
22580
|
-
if (request.query.locationName) conditions.push(
|
|
22661
|
+
const conditions = [eq23(gbpKeywordImpressions.projectId, project.id)];
|
|
22662
|
+
if (request.query.locationName) conditions.push(eq23(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
22581
22663
|
const rows = app.db.select().from(gbpKeywordImpressions).where(and17(...conditions)).all();
|
|
22582
22664
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
22583
22665
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
@@ -22596,8 +22678,8 @@ async function googleRoutes(app, opts) {
|
|
|
22596
22678
|
});
|
|
22597
22679
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
22598
22680
|
const project = resolveProject(app.db, request.params.name);
|
|
22599
|
-
const conditions = [
|
|
22600
|
-
if (request.query.locationName) conditions.push(
|
|
22681
|
+
const conditions = [eq23(gbpPlaceActions.projectId, project.id)];
|
|
22682
|
+
if (request.query.locationName) conditions.push(eq23(gbpPlaceActions.locationName, request.query.locationName));
|
|
22601
22683
|
const rows = app.db.select().from(gbpPlaceActions).where(and17(...conditions)).all();
|
|
22602
22684
|
return {
|
|
22603
22685
|
placeActions: rows.map((r) => ({
|
|
@@ -22613,8 +22695,8 @@ async function googleRoutes(app, opts) {
|
|
|
22613
22695
|
});
|
|
22614
22696
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
22615
22697
|
const project = resolveProject(app.db, request.params.name);
|
|
22616
|
-
const conditions = [
|
|
22617
|
-
if (request.query.locationName) conditions.push(
|
|
22698
|
+
const conditions = [eq23(gbpLodgingSnapshots.projectId, project.id)];
|
|
22699
|
+
if (request.query.locationName) conditions.push(eq23(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
22618
22700
|
const rows = app.db.select().from(gbpLodgingSnapshots).where(and17(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
22619
22701
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22620
22702
|
for (const row of rows) {
|
|
@@ -22630,8 +22712,8 @@ async function googleRoutes(app, opts) {
|
|
|
22630
22712
|
});
|
|
22631
22713
|
app.get("/projects/:name/gbp/attributes", async (request) => {
|
|
22632
22714
|
const project = resolveProject(app.db, request.params.name);
|
|
22633
|
-
const conditions = [
|
|
22634
|
-
if (request.query.locationName) conditions.push(
|
|
22715
|
+
const conditions = [eq23(gbpAttributesSnapshots.projectId, project.id)];
|
|
22716
|
+
if (request.query.locationName) conditions.push(eq23(gbpAttributesSnapshots.locationName, request.query.locationName));
|
|
22635
22717
|
const rows = app.db.select().from(gbpAttributesSnapshots).where(and17(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
|
|
22636
22718
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22637
22719
|
for (const row of rows) {
|
|
@@ -22650,8 +22732,8 @@ async function googleRoutes(app, opts) {
|
|
|
22650
22732
|
});
|
|
22651
22733
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
22652
22734
|
const project = resolveProject(app.db, request.params.name);
|
|
22653
|
-
const conditions = [
|
|
22654
|
-
if (request.query.locationName) conditions.push(
|
|
22735
|
+
const conditions = [eq23(gbpPlaceDetails.projectId, project.id)];
|
|
22736
|
+
if (request.query.locationName) conditions.push(eq23(gbpPlaceDetails.locationName, request.query.locationName));
|
|
22655
22737
|
const rows = app.db.select().from(gbpPlaceDetails).where(and17(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
22656
22738
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22657
22739
|
for (const row of rows) {
|
|
@@ -22671,7 +22753,7 @@ async function googleRoutes(app, opts) {
|
|
|
22671
22753
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
22672
22754
|
const project = resolveProject(app.db, request.params.name);
|
|
22673
22755
|
const locationName = request.query.locationName ?? null;
|
|
22674
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and17(
|
|
22756
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
22675
22757
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22676
22758
|
if (locationNames.length === 0) {
|
|
22677
22759
|
return buildGbpSummary({
|
|
@@ -22685,10 +22767,10 @@ async function googleRoutes(app, opts) {
|
|
|
22685
22767
|
locationProfiles: []
|
|
22686
22768
|
});
|
|
22687
22769
|
}
|
|
22688
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(and17(
|
|
22689
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and17(
|
|
22690
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and17(
|
|
22691
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and17(
|
|
22770
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and17(eq23(gbpDailyMetrics.projectId, project.id), inArray11(gbpDailyMetrics.locationName, locationNames))).all();
|
|
22771
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and17(eq23(gbpKeywordImpressions.projectId, project.id), inArray11(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
22772
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and17(eq23(gbpPlaceActions.projectId, project.id), inArray11(gbpPlaceActions.locationName, locationNames))).all();
|
|
22773
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and17(eq23(gbpLodgingSnapshots.projectId, project.id), inArray11(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
22692
22774
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
22693
22775
|
for (const row of lodgingRows) {
|
|
22694
22776
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -22702,7 +22784,7 @@ async function googleRoutes(app, opts) {
|
|
|
22702
22784
|
regularHours: gbpLocations.regularHours,
|
|
22703
22785
|
primaryPhone: gbpLocations.primaryPhone,
|
|
22704
22786
|
openStatus: gbpLocations.openStatus
|
|
22705
|
-
}).from(gbpLocations).where(and17(
|
|
22787
|
+
}).from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), inArray11(gbpLocations.locationName, locationNames))).all();
|
|
22706
22788
|
return buildGbpSummary({
|
|
22707
22789
|
locationName,
|
|
22708
22790
|
locationCount: locationNames.length,
|
|
@@ -22717,8 +22799,8 @@ async function googleRoutes(app, opts) {
|
|
|
22717
22799
|
}
|
|
22718
22800
|
|
|
22719
22801
|
// ../api-routes/src/ads.ts
|
|
22720
|
-
import
|
|
22721
|
-
import { eq as
|
|
22802
|
+
import crypto20 from "crypto";
|
|
22803
|
+
import { eq as eq24, and as and18, asc as asc3, gte as gte3, lte as lte2, inArray as inArray12 } from "drizzle-orm";
|
|
22722
22804
|
function statusDto(row) {
|
|
22723
22805
|
if (!row) return { connected: false };
|
|
22724
22806
|
return {
|
|
@@ -22768,7 +22850,7 @@ async function adsRoutes(app, opts) {
|
|
|
22768
22850
|
createdAt: existingCfg?.createdAt ?? now,
|
|
22769
22851
|
updatedAt: now
|
|
22770
22852
|
});
|
|
22771
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
22853
|
+
const existingRow = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22772
22854
|
app.db.transaction((tx) => {
|
|
22773
22855
|
if (existingRow) {
|
|
22774
22856
|
tx.update(adsConnections).set({
|
|
@@ -22778,10 +22860,10 @@ async function adsRoutes(app, opts) {
|
|
|
22778
22860
|
timezone: account.timezone,
|
|
22779
22861
|
status: account.status,
|
|
22780
22862
|
updatedAt: now
|
|
22781
|
-
}).where(
|
|
22863
|
+
}).where(eq24(adsConnections.id, existingRow.id)).run();
|
|
22782
22864
|
} else {
|
|
22783
22865
|
tx.insert(adsConnections).values({
|
|
22784
|
-
id:
|
|
22866
|
+
id: crypto20.randomUUID(),
|
|
22785
22867
|
projectId: project.id,
|
|
22786
22868
|
adAccountId: account.id,
|
|
22787
22869
|
displayName: account.name,
|
|
@@ -22800,16 +22882,16 @@ async function adsRoutes(app, opts) {
|
|
|
22800
22882
|
entityId: account.id
|
|
22801
22883
|
}));
|
|
22802
22884
|
});
|
|
22803
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22885
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22804
22886
|
return statusDto(row);
|
|
22805
22887
|
}
|
|
22806
22888
|
);
|
|
22807
22889
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
22808
22890
|
const project = resolveProject(app.db, request.params.name);
|
|
22809
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22891
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22810
22892
|
if (row) {
|
|
22811
22893
|
app.db.transaction((tx) => {
|
|
22812
|
-
tx.delete(adsConnections).where(
|
|
22894
|
+
tx.delete(adsConnections).where(eq24(adsConnections.id, row.id)).run();
|
|
22813
22895
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
22814
22896
|
projectId: project.id,
|
|
22815
22897
|
actor: "api",
|
|
@@ -22825,25 +22907,25 @@ async function adsRoutes(app, opts) {
|
|
|
22825
22907
|
});
|
|
22826
22908
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
22827
22909
|
const project = resolveProject(app.db, request.params.name);
|
|
22828
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22910
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22829
22911
|
return statusDto(row);
|
|
22830
22912
|
});
|
|
22831
22913
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
22832
22914
|
const project = resolveProject(app.db, request.params.name);
|
|
22833
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22915
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22834
22916
|
if (!row) {
|
|
22835
22917
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
22836
22918
|
}
|
|
22837
22919
|
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and18(
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22920
|
+
eq24(runs.projectId, project.id),
|
|
22921
|
+
eq24(runs.kind, RunKinds["ads-sync"]),
|
|
22922
|
+
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
22841
22923
|
)).get();
|
|
22842
22924
|
if (inFlight) {
|
|
22843
22925
|
const existing = { runId: inFlight.id, status: inFlight.status };
|
|
22844
22926
|
return existing;
|
|
22845
22927
|
}
|
|
22846
|
-
const runId =
|
|
22928
|
+
const runId = crypto20.randomUUID();
|
|
22847
22929
|
app.db.insert(runs).values({
|
|
22848
22930
|
id: runId,
|
|
22849
22931
|
projectId: project.id,
|
|
@@ -22858,9 +22940,9 @@ async function adsRoutes(app, opts) {
|
|
|
22858
22940
|
});
|
|
22859
22941
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
22860
22942
|
const project = resolveProject(app.db, request.params.name);
|
|
22861
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
22862
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
22863
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
22943
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq24(adsCampaigns.projectId, project.id)).all();
|
|
22944
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq24(adsAdGroups.projectId, project.id)).all();
|
|
22945
|
+
const adRows = app.db.select().from(adsAds).where(eq24(adsAds.projectId, project.id)).all();
|
|
22864
22946
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
22865
22947
|
for (const ad of adRows) {
|
|
22866
22948
|
const dto = {
|
|
@@ -22914,9 +22996,9 @@ async function adsRoutes(app, opts) {
|
|
|
22914
22996
|
}
|
|
22915
22997
|
parsedLevel = result.data;
|
|
22916
22998
|
}
|
|
22917
|
-
const conditions = [
|
|
22918
|
-
if (parsedLevel) conditions.push(
|
|
22919
|
-
if (entityId) conditions.push(
|
|
22999
|
+
const conditions = [eq24(adsInsightsDaily.projectId, project.id)];
|
|
23000
|
+
if (parsedLevel) conditions.push(eq24(adsInsightsDaily.level, parsedLevel));
|
|
23001
|
+
if (entityId) conditions.push(eq24(adsInsightsDaily.entityId, entityId));
|
|
22920
23002
|
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
22921
23003
|
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
22922
23004
|
const rows = app.db.select().from(adsInsightsDaily).where(and18(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
|
|
@@ -22931,19 +23013,19 @@ async function adsRoutes(app, opts) {
|
|
|
22931
23013
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
22932
23014
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
22933
23015
|
}));
|
|
22934
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
23016
|
+
const conn = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22935
23017
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
22936
23018
|
return response;
|
|
22937
23019
|
});
|
|
22938
23020
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
22939
23021
|
const project = resolveProject(app.db, request.params.name);
|
|
22940
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22941
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
22942
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
22943
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
23022
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
23023
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq24(adsCampaigns.projectId, project.id)).all().length;
|
|
23024
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq24(adsAdGroups.projectId, project.id)).all().length;
|
|
23025
|
+
const adCount = app.db.select().from(adsAds).where(eq24(adsAds.projectId, project.id)).all().length;
|
|
22944
23026
|
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and18(
|
|
22945
|
-
|
|
22946
|
-
|
|
23027
|
+
eq24(adsInsightsDaily.projectId, project.id),
|
|
23028
|
+
eq24(adsInsightsDaily.level, "campaign")
|
|
22947
23029
|
)).all();
|
|
22948
23030
|
let impressions = 0;
|
|
22949
23031
|
let clicks = 0;
|
|
@@ -22982,8 +23064,8 @@ async function adsRoutes(app, opts) {
|
|
|
22982
23064
|
}
|
|
22983
23065
|
|
|
22984
23066
|
// ../api-routes/src/bing.ts
|
|
22985
|
-
import
|
|
22986
|
-
import { eq as
|
|
23067
|
+
import crypto21 from "crypto";
|
|
23068
|
+
import { eq as eq25, and as and19, desc as desc12 } from "drizzle-orm";
|
|
22987
23069
|
|
|
22988
23070
|
// ../integration-bing/src/constants.ts
|
|
22989
23071
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -23357,7 +23439,7 @@ async function bingRoutes(app, opts) {
|
|
|
23357
23439
|
const store = requireConnectionStore();
|
|
23358
23440
|
const project = resolveProject(app.db, request.params.name);
|
|
23359
23441
|
requireConnection(store, project.canonicalDomain);
|
|
23360
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
23442
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
23361
23443
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
23362
23444
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
23363
23445
|
for (const row of allInspections) {
|
|
@@ -23414,7 +23496,7 @@ async function bingRoutes(app, opts) {
|
|
|
23414
23496
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
23415
23497
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23416
23498
|
app.db.insert(bingCoverageSnapshots).values({
|
|
23417
|
-
id:
|
|
23499
|
+
id: crypto21.randomUUID(),
|
|
23418
23500
|
projectId: project.id,
|
|
23419
23501
|
syncRunId: snapshotRunId,
|
|
23420
23502
|
date: snapshotDate,
|
|
@@ -23446,7 +23528,7 @@ async function bingRoutes(app, opts) {
|
|
|
23446
23528
|
const project = resolveProject(app.db, request.params.name);
|
|
23447
23529
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
23448
23530
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
23449
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
23531
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq25(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
23450
23532
|
return rows.map((r) => ({
|
|
23451
23533
|
date: r.date,
|
|
23452
23534
|
indexed: r.indexed,
|
|
@@ -23458,7 +23540,7 @@ async function bingRoutes(app, opts) {
|
|
|
23458
23540
|
requireConnectionStore();
|
|
23459
23541
|
const project = resolveProject(app.db, request.params.name);
|
|
23460
23542
|
const { url, limit } = request.query;
|
|
23461
|
-
const whereClause = url ? and19(
|
|
23543
|
+
const whereClause = url ? and19(eq25(bingUrlInspections.projectId, project.id), eq25(bingUrlInspections.url, url)) : eq25(bingUrlInspections.projectId, project.id);
|
|
23462
23544
|
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();
|
|
23463
23545
|
return filtered.map((r) => ({
|
|
23464
23546
|
id: r.id,
|
|
@@ -23485,7 +23567,7 @@ async function bingRoutes(app, opts) {
|
|
|
23485
23567
|
throw validationError("url is required");
|
|
23486
23568
|
}
|
|
23487
23569
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23488
|
-
const runId =
|
|
23570
|
+
const runId = crypto21.randomUUID();
|
|
23489
23571
|
app.db.insert(runs).values({
|
|
23490
23572
|
id: runId,
|
|
23491
23573
|
projectId: project.id,
|
|
@@ -23506,7 +23588,7 @@ async function bingRoutes(app, opts) {
|
|
|
23506
23588
|
discoveryDate: result.DiscoveryDate ?? null
|
|
23507
23589
|
});
|
|
23508
23590
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23509
|
-
const id =
|
|
23591
|
+
const id = crypto21.randomUUID();
|
|
23510
23592
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
23511
23593
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
23512
23594
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
@@ -23548,7 +23630,7 @@ async function bingRoutes(app, opts) {
|
|
|
23548
23630
|
anchorCount: result.AnchorCount ?? null,
|
|
23549
23631
|
discoveryDate
|
|
23550
23632
|
}).run();
|
|
23551
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
23633
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq25(runs.id, runId)).run();
|
|
23552
23634
|
return {
|
|
23553
23635
|
id,
|
|
23554
23636
|
url,
|
|
@@ -23564,7 +23646,7 @@ async function bingRoutes(app, opts) {
|
|
|
23564
23646
|
} catch (e) {
|
|
23565
23647
|
const msg = e instanceof Error ? e.message : String(e);
|
|
23566
23648
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
23567
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
23649
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
|
|
23568
23650
|
throw e;
|
|
23569
23651
|
}
|
|
23570
23652
|
});
|
|
@@ -23576,7 +23658,7 @@ async function bingRoutes(app, opts) {
|
|
|
23576
23658
|
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
23577
23659
|
}
|
|
23578
23660
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23579
|
-
const runId =
|
|
23661
|
+
const runId = crypto21.randomUUID();
|
|
23580
23662
|
app.db.insert(runs).values({
|
|
23581
23663
|
id: runId,
|
|
23582
23664
|
projectId: project.id,
|
|
@@ -23591,7 +23673,7 @@ async function bingRoutes(app, opts) {
|
|
|
23591
23673
|
} else {
|
|
23592
23674
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
23593
23675
|
}
|
|
23594
|
-
const run = app.db.select().from(runs).where(
|
|
23676
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
23595
23677
|
return run;
|
|
23596
23678
|
});
|
|
23597
23679
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -23603,7 +23685,7 @@ async function bingRoutes(app, opts) {
|
|
|
23603
23685
|
}
|
|
23604
23686
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
23605
23687
|
if (request.body?.allUnindexed) {
|
|
23606
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
23688
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
23607
23689
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
23608
23690
|
for (const row of allInspections) {
|
|
23609
23691
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -23690,14 +23772,14 @@ async function bingRoutes(app, opts) {
|
|
|
23690
23772
|
import fs from "fs";
|
|
23691
23773
|
import path from "path";
|
|
23692
23774
|
import os from "os";
|
|
23693
|
-
import { eq as
|
|
23775
|
+
import { eq as eq26, and as and20 } from "drizzle-orm";
|
|
23694
23776
|
function getScreenshotDir() {
|
|
23695
23777
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
23696
23778
|
}
|
|
23697
23779
|
async function cdpRoutes(app, opts) {
|
|
23698
23780
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
23699
23781
|
const { snapshotId } = request.params;
|
|
23700
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs,
|
|
23782
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq26(querySnapshots.runId, runs.id)).where(eq26(querySnapshots.id, snapshotId)).get();
|
|
23701
23783
|
if (!snapshot?.screenshotPath) {
|
|
23702
23784
|
const err = notFound("Screenshot", snapshotId);
|
|
23703
23785
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -23765,7 +23847,7 @@ async function cdpRoutes(app, opts) {
|
|
|
23765
23847
|
async (request, reply) => {
|
|
23766
23848
|
const project = resolveProject(app.db, request.params.name);
|
|
23767
23849
|
const { runId } = request.params;
|
|
23768
|
-
const run = app.db.select().from(runs).where(and20(
|
|
23850
|
+
const run = app.db.select().from(runs).where(and20(eq26(runs.id, runId), eq26(runs.projectId, project.id))).get();
|
|
23769
23851
|
if (!run) {
|
|
23770
23852
|
const err = notFound("Run", runId);
|
|
23771
23853
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -23778,8 +23860,8 @@ async function cdpRoutes(app, opts) {
|
|
|
23778
23860
|
citedDomains: querySnapshots.citedDomains,
|
|
23779
23861
|
screenshotPath: querySnapshots.screenshotPath,
|
|
23780
23862
|
rawResponse: querySnapshots.rawResponse
|
|
23781
|
-
}).from(querySnapshots).where(
|
|
23782
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
23863
|
+
}).from(querySnapshots).where(eq26(querySnapshots.runId, runId)).all());
|
|
23864
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq26(queries.projectId, project.id)).all();
|
|
23783
23865
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
23784
23866
|
const byQuery = /* @__PURE__ */ new Map();
|
|
23785
23867
|
for (const snap of snapshots) {
|
|
@@ -23861,8 +23943,8 @@ async function cdpRoutes(app, opts) {
|
|
|
23861
23943
|
}
|
|
23862
23944
|
|
|
23863
23945
|
// ../api-routes/src/ga.ts
|
|
23864
|
-
import
|
|
23865
|
-
import { eq as
|
|
23946
|
+
import crypto22 from "crypto";
|
|
23947
|
+
import { eq as eq27, desc as desc13, and as and21, sql as sql10 } from "drizzle-orm";
|
|
23866
23948
|
function gaLog(level, action, ctx) {
|
|
23867
23949
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
23868
23950
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -24139,10 +24221,10 @@ async function ga4Routes(app, opts) {
|
|
|
24139
24221
|
if (!saConn && !oauthConn) {
|
|
24140
24222
|
throw notFound("GA4 connection", project.name);
|
|
24141
24223
|
}
|
|
24142
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
24143
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
24144
|
-
app.db.delete(gaAiReferrals).where(
|
|
24145
|
-
app.db.delete(gaSocialReferrals).where(
|
|
24224
|
+
app.db.delete(gaTrafficSnapshots).where(eq27(gaTrafficSnapshots.projectId, project.id)).run();
|
|
24225
|
+
app.db.delete(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).run();
|
|
24226
|
+
app.db.delete(gaAiReferrals).where(eq27(gaAiReferrals.projectId, project.id)).run();
|
|
24227
|
+
app.db.delete(gaSocialReferrals).where(eq27(gaSocialReferrals.projectId, project.id)).run();
|
|
24146
24228
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
24147
24229
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
24148
24230
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -24163,7 +24245,7 @@ async function ga4Routes(app, opts) {
|
|
|
24163
24245
|
if (!connected) {
|
|
24164
24246
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
24165
24247
|
}
|
|
24166
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
24248
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
24167
24249
|
return {
|
|
24168
24250
|
connected: true,
|
|
24169
24251
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -24187,7 +24269,7 @@ async function ga4Routes(app, opts) {
|
|
|
24187
24269
|
const syncAi = !only || only === "ai";
|
|
24188
24270
|
const syncSocial = !only || only === "social";
|
|
24189
24271
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24190
|
-
const runId =
|
|
24272
|
+
const runId = crypto22.randomUUID();
|
|
24191
24273
|
app.db.insert(runs).values({
|
|
24192
24274
|
id: runId,
|
|
24193
24275
|
projectId: project.id,
|
|
@@ -24228,14 +24310,14 @@ async function ga4Routes(app, opts) {
|
|
|
24228
24310
|
if (syncTraffic) {
|
|
24229
24311
|
tx.delete(gaTrafficSnapshots).where(
|
|
24230
24312
|
and21(
|
|
24231
|
-
|
|
24313
|
+
eq27(gaTrafficSnapshots.projectId, project.id),
|
|
24232
24314
|
sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
24233
24315
|
sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
24234
24316
|
)
|
|
24235
24317
|
).run();
|
|
24236
24318
|
for (const row of rows) {
|
|
24237
24319
|
tx.insert(gaTrafficSnapshots).values({
|
|
24238
|
-
id:
|
|
24320
|
+
id: crypto22.randomUUID(),
|
|
24239
24321
|
projectId: project.id,
|
|
24240
24322
|
date: row.date,
|
|
24241
24323
|
landingPage: row.landingPage,
|
|
@@ -24252,14 +24334,14 @@ async function ga4Routes(app, opts) {
|
|
|
24252
24334
|
if (syncAi) {
|
|
24253
24335
|
tx.delete(gaAiReferrals).where(
|
|
24254
24336
|
and21(
|
|
24255
|
-
|
|
24337
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24256
24338
|
sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
24257
24339
|
sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
24258
24340
|
)
|
|
24259
24341
|
).run();
|
|
24260
24342
|
for (const row of aiReferrals) {
|
|
24261
24343
|
tx.insert(gaAiReferrals).values({
|
|
24262
|
-
id:
|
|
24344
|
+
id: crypto22.randomUUID(),
|
|
24263
24345
|
projectId: project.id,
|
|
24264
24346
|
date: row.date,
|
|
24265
24347
|
source: row.source,
|
|
@@ -24284,14 +24366,14 @@ async function ga4Routes(app, opts) {
|
|
|
24284
24366
|
if (syncSocial) {
|
|
24285
24367
|
tx.delete(gaSocialReferrals).where(
|
|
24286
24368
|
and21(
|
|
24287
|
-
|
|
24369
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24288
24370
|
sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
24289
24371
|
sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
24290
24372
|
)
|
|
24291
24373
|
).run();
|
|
24292
24374
|
for (const row of socialReferrals) {
|
|
24293
24375
|
tx.insert(gaSocialReferrals).values({
|
|
24294
|
-
id:
|
|
24376
|
+
id: crypto22.randomUUID(),
|
|
24295
24377
|
projectId: project.id,
|
|
24296
24378
|
date: row.date,
|
|
24297
24379
|
source: row.source,
|
|
@@ -24305,9 +24387,9 @@ async function ga4Routes(app, opts) {
|
|
|
24305
24387
|
}
|
|
24306
24388
|
}
|
|
24307
24389
|
if (syncSummary) {
|
|
24308
|
-
tx.delete(gaTrafficSummaries).where(
|
|
24390
|
+
tx.delete(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).run();
|
|
24309
24391
|
tx.insert(gaTrafficSummaries).values({
|
|
24310
|
-
id:
|
|
24392
|
+
id: crypto22.randomUUID(),
|
|
24311
24393
|
projectId: project.id,
|
|
24312
24394
|
periodStart: summary.periodStart,
|
|
24313
24395
|
periodEnd: summary.periodEnd,
|
|
@@ -24317,10 +24399,10 @@ async function ga4Routes(app, opts) {
|
|
|
24317
24399
|
syncedAt: now,
|
|
24318
24400
|
syncRunId: runId
|
|
24319
24401
|
}).run();
|
|
24320
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
24402
|
+
tx.delete(gaTrafficWindowSummaries).where(eq27(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
24321
24403
|
for (const ws of windowSummaries) {
|
|
24322
24404
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
24323
|
-
id:
|
|
24405
|
+
id: crypto22.randomUUID(),
|
|
24324
24406
|
projectId: project.id,
|
|
24325
24407
|
windowKey: ws.windowKey,
|
|
24326
24408
|
periodStart: ws.periodStart,
|
|
@@ -24335,7 +24417,7 @@ async function ga4Routes(app, opts) {
|
|
|
24335
24417
|
}
|
|
24336
24418
|
}
|
|
24337
24419
|
});
|
|
24338
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
24420
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq27(runs.id, runId)).run();
|
|
24339
24421
|
const syncedComponents = only ? [
|
|
24340
24422
|
...syncTraffic ? ["traffic"] : [],
|
|
24341
24423
|
...syncSummary ? ["summary"] : [],
|
|
@@ -24364,7 +24446,7 @@ async function ga4Routes(app, opts) {
|
|
|
24364
24446
|
} catch (e) {
|
|
24365
24447
|
const msg = e instanceof Error ? e.message : String(e);
|
|
24366
24448
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
24367
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
24449
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
|
|
24368
24450
|
throw e;
|
|
24369
24451
|
}
|
|
24370
24452
|
});
|
|
@@ -24375,11 +24457,11 @@ async function ga4Routes(app, opts) {
|
|
|
24375
24457
|
const window = parseWindow(request.query.window);
|
|
24376
24458
|
const cutoff = windowCutoff(window);
|
|
24377
24459
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
24378
|
-
const snapshotConditions = [
|
|
24460
|
+
const snapshotConditions = [eq27(gaTrafficSnapshots.projectId, project.id)];
|
|
24379
24461
|
if (cutoffDate) snapshotConditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
24380
|
-
const aiConditions = [
|
|
24462
|
+
const aiConditions = [eq27(gaAiReferrals.projectId, project.id)];
|
|
24381
24463
|
if (cutoffDate) aiConditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
24382
|
-
const socialConditions = [
|
|
24464
|
+
const socialConditions = [eq27(gaSocialReferrals.projectId, project.id)];
|
|
24383
24465
|
if (cutoffDate) socialConditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
24384
24466
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
24385
24467
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -24388,8 +24470,8 @@ async function ga4Routes(app, opts) {
|
|
|
24388
24470
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
24389
24471
|
}).from(gaTrafficWindowSummaries).where(
|
|
24390
24472
|
and21(
|
|
24391
|
-
|
|
24392
|
-
|
|
24473
|
+
eq27(gaTrafficWindowSummaries.projectId, project.id),
|
|
24474
|
+
eq27(gaTrafficWindowSummaries.windowKey, window)
|
|
24393
24475
|
)
|
|
24394
24476
|
).get() : null;
|
|
24395
24477
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
@@ -24401,14 +24483,14 @@ async function ga4Routes(app, opts) {
|
|
|
24401
24483
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
24402
24484
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
24403
24485
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
24404
|
-
}).from(gaTrafficSummaries).where(
|
|
24486
|
+
}).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).get();
|
|
24405
24487
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
24406
24488
|
totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
24407
24489
|
}).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).get();
|
|
24408
24490
|
const summaryMeta = app.db.select({
|
|
24409
24491
|
periodStart: gaTrafficSummaries.periodStart,
|
|
24410
24492
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
24411
|
-
}).from(gaTrafficSummaries).where(
|
|
24493
|
+
}).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).get();
|
|
24412
24494
|
const rows = app.db.select({
|
|
24413
24495
|
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
24414
24496
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
@@ -24469,7 +24551,7 @@ async function ga4Routes(app, opts) {
|
|
|
24469
24551
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
24470
24552
|
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
24471
24553
|
}).from(gaSocialReferrals).where(and21(...socialConditions)).get();
|
|
24472
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
24554
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
24473
24555
|
const total = summaryRow?.totalSessions ?? 0;
|
|
24474
24556
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
24475
24557
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -24567,7 +24649,7 @@ async function ga4Routes(app, opts) {
|
|
|
24567
24649
|
const project = resolveProject(app.db, request.params.name);
|
|
24568
24650
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24569
24651
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24570
|
-
const conditions = [
|
|
24652
|
+
const conditions = [eq27(gaAiReferrals.projectId, project.id)];
|
|
24571
24653
|
if (cutoffDate) conditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
24572
24654
|
const rows = app.db.select({
|
|
24573
24655
|
date: gaAiReferrals.date,
|
|
@@ -24592,7 +24674,7 @@ async function ga4Routes(app, opts) {
|
|
|
24592
24674
|
const project = resolveProject(app.db, request.params.name);
|
|
24593
24675
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24594
24676
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24595
|
-
const conditions = [
|
|
24677
|
+
const conditions = [eq27(gaSocialReferrals.projectId, project.id)];
|
|
24596
24678
|
if (cutoffDate) conditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
24597
24679
|
const rows = app.db.select({
|
|
24598
24680
|
date: gaSocialReferrals.date,
|
|
@@ -24615,7 +24697,7 @@ async function ga4Routes(app, opts) {
|
|
|
24615
24697
|
return fmt(d);
|
|
24616
24698
|
};
|
|
24617
24699
|
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(
|
|
24618
|
-
|
|
24700
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24619
24701
|
sql10`${gaSocialReferrals.date} >= ${from}`,
|
|
24620
24702
|
sql10`${gaSocialReferrals.date} < ${to}`
|
|
24621
24703
|
)).get();
|
|
@@ -24628,7 +24710,7 @@ async function ga4Routes(app, opts) {
|
|
|
24628
24710
|
source: gaSocialReferrals.source,
|
|
24629
24711
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
24630
24712
|
}).from(gaSocialReferrals).where(and21(
|
|
24631
|
-
|
|
24713
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24632
24714
|
sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
24633
24715
|
sql10`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
24634
24716
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -24636,7 +24718,7 @@ async function ga4Routes(app, opts) {
|
|
|
24636
24718
|
source: gaSocialReferrals.source,
|
|
24637
24719
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
24638
24720
|
}).from(gaSocialReferrals).where(and21(
|
|
24639
|
-
|
|
24721
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24640
24722
|
sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
24641
24723
|
sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
24642
24724
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -24677,16 +24759,16 @@ async function ga4Routes(app, opts) {
|
|
|
24677
24759
|
return fmt(d);
|
|
24678
24760
|
};
|
|
24679
24761
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
24680
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and21(
|
|
24681
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(
|
|
24682
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(
|
|
24762
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
24763
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
24764
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
24683
24765
|
const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
|
|
24684
|
-
|
|
24766
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24685
24767
|
sql10`${gaAiReferrals.date} >= ${from}`,
|
|
24686
24768
|
sql10`${gaAiReferrals.date} < ${to}`,
|
|
24687
|
-
|
|
24769
|
+
eq27(gaAiReferrals.sourceDimension, "session")
|
|
24688
24770
|
)).get();
|
|
24689
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(
|
|
24771
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
|
|
24690
24772
|
const todayStr = fmt(today);
|
|
24691
24773
|
const buildTrend = (sum) => {
|
|
24692
24774
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -24696,16 +24778,16 @@ async function ga4Routes(app, opts) {
|
|
|
24696
24778
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
24697
24779
|
};
|
|
24698
24780
|
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
|
|
24699
|
-
|
|
24781
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24700
24782
|
sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
24701
24783
|
sql10`${gaAiReferrals.date} < ${todayStr}`,
|
|
24702
|
-
|
|
24784
|
+
eq27(gaAiReferrals.sourceDimension, "session")
|
|
24703
24785
|
)).groupBy(gaAiReferrals.source).all();
|
|
24704
24786
|
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
|
|
24705
|
-
|
|
24787
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24706
24788
|
sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
24707
24789
|
sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
24708
|
-
|
|
24790
|
+
eq27(gaAiReferrals.sourceDimension, "session")
|
|
24709
24791
|
)).groupBy(gaAiReferrals.source).all();
|
|
24710
24792
|
const findBiggestMover = (current, prev) => {
|
|
24711
24793
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -24721,8 +24803,8 @@ async function ga4Routes(app, opts) {
|
|
|
24721
24803
|
}
|
|
24722
24804
|
return mover;
|
|
24723
24805
|
};
|
|
24724
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(
|
|
24725
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(
|
|
24806
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
24807
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
24726
24808
|
return {
|
|
24727
24809
|
total: buildTrend(sumTotal),
|
|
24728
24810
|
organic: buildTrend(sumOrganic),
|
|
@@ -24737,7 +24819,7 @@ async function ga4Routes(app, opts) {
|
|
|
24737
24819
|
const project = resolveProject(app.db, request.params.name);
|
|
24738
24820
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24739
24821
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24740
|
-
const conditions = [
|
|
24822
|
+
const conditions = [eq27(gaTrafficSnapshots.projectId, project.id)];
|
|
24741
24823
|
if (cutoffDate) conditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
24742
24824
|
const rows = app.db.select({
|
|
24743
24825
|
date: gaTrafficSnapshots.date,
|
|
@@ -24760,7 +24842,7 @@ async function ga4Routes(app, opts) {
|
|
|
24760
24842
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
24761
24843
|
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
24762
24844
|
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
24763
|
-
}).from(gaTrafficSnapshots).where(
|
|
24845
|
+
}).from(gaTrafficSnapshots).where(eq27(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
24764
24846
|
return {
|
|
24765
24847
|
pages: trafficPages.map((r) => ({
|
|
24766
24848
|
landingPage: r.landingPage,
|
|
@@ -24894,7 +24976,7 @@ function parseSchemaPageEntry(entry) {
|
|
|
24894
24976
|
}
|
|
24895
24977
|
|
|
24896
24978
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
24897
|
-
import
|
|
24979
|
+
import crypto23 from "crypto";
|
|
24898
24980
|
function validateUsername(username) {
|
|
24899
24981
|
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
24900
24982
|
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
@@ -25107,7 +25189,7 @@ function buildSnippet(content) {
|
|
|
25107
25189
|
return `${text2.slice(0, 157)}...`;
|
|
25108
25190
|
}
|
|
25109
25191
|
function contentHash(content) {
|
|
25110
|
-
return
|
|
25192
|
+
return crypto23.createHash("sha256").update(content).digest("hex");
|
|
25111
25193
|
}
|
|
25112
25194
|
function buildAmbiguousSlugMessage(slug, pages) {
|
|
25113
25195
|
const candidates = pages.map((page) => {
|
|
@@ -26407,8 +26489,8 @@ async function wordpressRoutes(app, opts) {
|
|
|
26407
26489
|
}
|
|
26408
26490
|
|
|
26409
26491
|
// ../api-routes/src/backlinks.ts
|
|
26410
|
-
import
|
|
26411
|
-
import { and as and23, asc as asc4, desc as desc14, eq as
|
|
26492
|
+
import crypto24 from "crypto";
|
|
26493
|
+
import { and as and23, asc as asc4, desc as desc14, eq as eq28, sql as sql11 } from "drizzle-orm";
|
|
26412
26494
|
|
|
26413
26495
|
// ../integration-commoncrawl/src/constants.ts
|
|
26414
26496
|
import os2 from "os";
|
|
@@ -26905,10 +26987,10 @@ function mapRunRow(row) {
|
|
|
26905
26987
|
}
|
|
26906
26988
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
26907
26989
|
const conditions = [
|
|
26908
|
-
|
|
26909
|
-
|
|
26990
|
+
eq28(backlinkSummaries.projectId, projectId),
|
|
26991
|
+
eq28(backlinkSummaries.source, source)
|
|
26910
26992
|
];
|
|
26911
|
-
if (release) conditions.push(
|
|
26993
|
+
if (release) conditions.push(eq28(backlinkSummaries.release, release));
|
|
26912
26994
|
return db.select().from(backlinkSummaries).where(and23(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
26913
26995
|
}
|
|
26914
26996
|
function parseExcludeCrawlers(value) {
|
|
@@ -26918,9 +27000,9 @@ function parseExcludeCrawlers(value) {
|
|
|
26918
27000
|
}
|
|
26919
27001
|
function computeFilteredSummary(db, base) {
|
|
26920
27002
|
const baseDomainCondition = and23(
|
|
26921
|
-
|
|
26922
|
-
|
|
26923
|
-
|
|
27003
|
+
eq28(backlinkDomains.projectId, base.projectId),
|
|
27004
|
+
eq28(backlinkDomains.source, base.source),
|
|
27005
|
+
eq28(backlinkDomains.release, base.release)
|
|
26924
27006
|
);
|
|
26925
27007
|
const filteredCondition = and23(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
26926
27008
|
const unfilteredAgg = db.select({
|
|
@@ -26952,13 +27034,13 @@ function computeFilteredSummary(db, base) {
|
|
|
26952
27034
|
};
|
|
26953
27035
|
}
|
|
26954
27036
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
26955
|
-
const summary = db.select().from(backlinkSummaries).where(and23(
|
|
27037
|
+
const summary = db.select().from(backlinkSummaries).where(and23(eq28(backlinkSummaries.projectId, projectId), eq28(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
26956
27038
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
26957
27039
|
if (summary && excludeCrawlers) {
|
|
26958
27040
|
const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and23(
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
27041
|
+
eq28(backlinkDomains.projectId, projectId),
|
|
27042
|
+
eq28(backlinkDomains.source, source),
|
|
27043
|
+
eq28(backlinkDomains.release, summary.release),
|
|
26962
27044
|
backlinkCrawlerExclusionClause()
|
|
26963
27045
|
)).get();
|
|
26964
27046
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -26973,7 +27055,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
26973
27055
|
};
|
|
26974
27056
|
}
|
|
26975
27057
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
26976
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
27058
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
26977
27059
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
26978
27060
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
26979
27061
|
const sources = [
|
|
@@ -27029,7 +27111,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27029
27111
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
27030
27112
|
);
|
|
27031
27113
|
}
|
|
27032
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
27114
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.release, release)).get();
|
|
27033
27115
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
27034
27116
|
if (existing) {
|
|
27035
27117
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -27040,12 +27122,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
27040
27122
|
phaseDetail: null,
|
|
27041
27123
|
error: null,
|
|
27042
27124
|
updatedAt: now
|
|
27043
|
-
}).where(
|
|
27125
|
+
}).where(eq28(ccReleaseSyncs.id, existing.id)).run();
|
|
27044
27126
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
27045
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
27127
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.id, existing.id)).get();
|
|
27046
27128
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
27047
27129
|
}
|
|
27048
|
-
const id =
|
|
27130
|
+
const id = crypto24.randomUUID();
|
|
27049
27131
|
app.db.insert(ccReleaseSyncs).values({
|
|
27050
27132
|
id,
|
|
27051
27133
|
release,
|
|
@@ -27054,7 +27136,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27054
27136
|
updatedAt: now
|
|
27055
27137
|
}).run();
|
|
27056
27138
|
opts.onReleaseSyncRequested(id, release);
|
|
27057
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
27139
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.id, id)).get();
|
|
27058
27140
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
27059
27141
|
});
|
|
27060
27142
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -27102,7 +27184,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27102
27184
|
throw validationError("Invalid release id");
|
|
27103
27185
|
}
|
|
27104
27186
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
27105
|
-
const runId =
|
|
27187
|
+
const runId = crypto24.randomUUID();
|
|
27106
27188
|
app.db.insert(runs).values({
|
|
27107
27189
|
id: runId,
|
|
27108
27190
|
projectId: project.id,
|
|
@@ -27112,7 +27194,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27112
27194
|
createdAt: now
|
|
27113
27195
|
}).run();
|
|
27114
27196
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
27115
|
-
const run = app.db.select().from(runs).where(
|
|
27197
|
+
const run = app.db.select().from(runs).where(eq28(runs.id, runId)).get();
|
|
27116
27198
|
return reply.status(201).send(mapRunRow(run));
|
|
27117
27199
|
});
|
|
27118
27200
|
app.get(
|
|
@@ -27139,9 +27221,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
27139
27221
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
27140
27222
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
27141
27223
|
const baseDomainCondition = and23(
|
|
27142
|
-
|
|
27143
|
-
|
|
27144
|
-
|
|
27224
|
+
eq28(backlinkDomains.projectId, project.id),
|
|
27225
|
+
eq28(backlinkDomains.source, source),
|
|
27226
|
+
eq28(backlinkDomains.release, targetRelease)
|
|
27145
27227
|
);
|
|
27146
27228
|
const domainCondition = excludeCrawlers ? and23(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
27147
27229
|
const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
@@ -27167,7 +27249,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27167
27249
|
async (request, reply) => {
|
|
27168
27250
|
const project = resolveProject(app.db, request.params.name);
|
|
27169
27251
|
const source = parseSourceParam(request.query.source);
|
|
27170
|
-
const rows = app.db.select().from(backlinkSummaries).where(and23(
|
|
27252
|
+
const rows = app.db.select().from(backlinkSummaries).where(and23(eq28(backlinkSummaries.projectId, project.id), eq28(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
|
|
27171
27253
|
const response = rows.map((r) => ({
|
|
27172
27254
|
release: r.release,
|
|
27173
27255
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -27204,7 +27286,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27204
27286
|
);
|
|
27205
27287
|
}
|
|
27206
27288
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
27207
|
-
const runId =
|
|
27289
|
+
const runId = crypto24.randomUUID();
|
|
27208
27290
|
app.db.insert(runs).values({
|
|
27209
27291
|
id: runId,
|
|
27210
27292
|
projectId: project.id,
|
|
@@ -27214,19 +27296,19 @@ async function backlinksRoutes(app, opts) {
|
|
|
27214
27296
|
createdAt: now
|
|
27215
27297
|
}).run();
|
|
27216
27298
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
27217
|
-
const run = app.db.select().from(runs).where(
|
|
27299
|
+
const run = app.db.select().from(runs).where(eq28(runs.id, runId)).get();
|
|
27218
27300
|
return reply.status(201).send(mapRunRow(run));
|
|
27219
27301
|
}
|
|
27220
27302
|
);
|
|
27221
27303
|
}
|
|
27222
27304
|
|
|
27223
27305
|
// ../api-routes/src/traffic.ts
|
|
27224
|
-
import
|
|
27306
|
+
import crypto26 from "crypto";
|
|
27225
27307
|
import { Agent as UndiciAgent } from "undici";
|
|
27226
|
-
import { and as and24, desc as desc15, eq as
|
|
27308
|
+
import { and as and24, desc as desc15, eq as eq29, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
|
|
27227
27309
|
|
|
27228
27310
|
// ../integration-cloud-run/src/auth.ts
|
|
27229
|
-
import
|
|
27311
|
+
import crypto25 from "crypto";
|
|
27230
27312
|
var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
27231
27313
|
var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
27232
27314
|
var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -27257,7 +27339,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
27257
27339
|
const headerB64 = encode(header);
|
|
27258
27340
|
const payloadB64 = encode(payload);
|
|
27259
27341
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
27260
|
-
const sign =
|
|
27342
|
+
const sign = crypto25.createSign("RSA-SHA256");
|
|
27261
27343
|
sign.update(signingInput);
|
|
27262
27344
|
const signature = sign.sign(privateKey, "base64url");
|
|
27263
27345
|
return `${signingInput}.${signature}`;
|
|
@@ -31090,8 +31172,8 @@ async function runBackfillTask(options) {
|
|
|
31090
31172
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31091
31173
|
try {
|
|
31092
31174
|
app.db.transaction((tx) => {
|
|
31093
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
31094
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
31175
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq29(runs.id, runId)).run();
|
|
31176
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
31095
31177
|
});
|
|
31096
31178
|
} catch {
|
|
31097
31179
|
}
|
|
@@ -31106,7 +31188,7 @@ async function runBackfillTask(options) {
|
|
|
31106
31188
|
if (allEvents.length === 0) {
|
|
31107
31189
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
31108
31190
|
try {
|
|
31109
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
31191
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq29(runs.id, runId)).run();
|
|
31110
31192
|
} catch {
|
|
31111
31193
|
}
|
|
31112
31194
|
return;
|
|
@@ -31129,28 +31211,28 @@ async function runBackfillTask(options) {
|
|
|
31129
31211
|
app.db.transaction((tx) => {
|
|
31130
31212
|
tx.delete(crawlerEventsHourly).where(
|
|
31131
31213
|
and24(
|
|
31132
|
-
|
|
31214
|
+
eq29(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
31133
31215
|
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
31134
31216
|
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31135
31217
|
)
|
|
31136
31218
|
).run();
|
|
31137
31219
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
31138
31220
|
and24(
|
|
31139
|
-
|
|
31221
|
+
eq29(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
31140
31222
|
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
31141
31223
|
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
31142
31224
|
)
|
|
31143
31225
|
).run();
|
|
31144
31226
|
tx.delete(aiReferralEventsHourly).where(
|
|
31145
31227
|
and24(
|
|
31146
|
-
|
|
31228
|
+
eq29(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
31147
31229
|
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
31148
31230
|
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
31149
31231
|
)
|
|
31150
31232
|
).run();
|
|
31151
31233
|
tx.delete(rawEventSamples).where(
|
|
31152
31234
|
and24(
|
|
31153
|
-
|
|
31235
|
+
eq29(rawEventSamples.sourceId, sourceRow.id),
|
|
31154
31236
|
gte4(rawEventSamples.ts, windowStartIso),
|
|
31155
31237
|
lte3(rawEventSamples.ts, windowEndIso)
|
|
31156
31238
|
)
|
|
@@ -31217,7 +31299,7 @@ async function runBackfillTask(options) {
|
|
|
31217
31299
|
}
|
|
31218
31300
|
})();
|
|
31219
31301
|
tx.insert(rawEventSamples).values({
|
|
31220
|
-
id:
|
|
31302
|
+
id: crypto26.randomUUID(),
|
|
31221
31303
|
projectId: project.id,
|
|
31222
31304
|
sourceId: sourceRow.id,
|
|
31223
31305
|
ts: sample.observedAt,
|
|
@@ -31241,8 +31323,8 @@ async function runBackfillTask(options) {
|
|
|
31241
31323
|
lastError: null,
|
|
31242
31324
|
lastEventIds: newRingBuffer,
|
|
31243
31325
|
updatedAt: finishedAt
|
|
31244
|
-
}).where(
|
|
31245
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
31326
|
+
}).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
31327
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
|
|
31246
31328
|
});
|
|
31247
31329
|
} catch (e) {
|
|
31248
31330
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -31324,7 +31406,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31324
31406
|
createdAt: existing?.createdAt ?? now,
|
|
31325
31407
|
updatedAt: now
|
|
31326
31408
|
});
|
|
31327
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
31409
|
+
const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
31328
31410
|
const config = {
|
|
31329
31411
|
gcpProjectId,
|
|
31330
31412
|
serviceName: serviceName ?? null,
|
|
@@ -31340,10 +31422,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31340
31422
|
lastError: null,
|
|
31341
31423
|
configJson: config,
|
|
31342
31424
|
updatedAt: now
|
|
31343
|
-
}).where(
|
|
31344
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31425
|
+
}).where(eq29(trafficSources.id, activeSource.id)).run();
|
|
31426
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
|
|
31345
31427
|
} else {
|
|
31346
|
-
const newId =
|
|
31428
|
+
const newId = crypto26.randomUUID();
|
|
31347
31429
|
app.db.insert(trafficSources).values({
|
|
31348
31430
|
id: newId,
|
|
31349
31431
|
projectId: project.id,
|
|
@@ -31358,7 +31440,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31358
31440
|
createdAt: now,
|
|
31359
31441
|
updatedAt: now
|
|
31360
31442
|
}).run();
|
|
31361
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31443
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
|
|
31362
31444
|
}
|
|
31363
31445
|
writeAuditLog(app.db, {
|
|
31364
31446
|
projectId: project.id,
|
|
@@ -31410,7 +31492,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31410
31492
|
createdAt: existing?.createdAt ?? now,
|
|
31411
31493
|
updatedAt: now
|
|
31412
31494
|
});
|
|
31413
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
31495
|
+
const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
31414
31496
|
const config = { baseUrl, username };
|
|
31415
31497
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
31416
31498
|
let sourceRow;
|
|
@@ -31421,10 +31503,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31421
31503
|
lastError: null,
|
|
31422
31504
|
configJson: config,
|
|
31423
31505
|
updatedAt: now
|
|
31424
|
-
}).where(
|
|
31425
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31506
|
+
}).where(eq29(trafficSources.id, activeSource.id)).run();
|
|
31507
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
|
|
31426
31508
|
} else {
|
|
31427
|
-
const newId =
|
|
31509
|
+
const newId = crypto26.randomUUID();
|
|
31428
31510
|
app.db.insert(trafficSources).values({
|
|
31429
31511
|
id: newId,
|
|
31430
31512
|
projectId: project.id,
|
|
@@ -31439,7 +31521,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31439
31521
|
createdAt: now,
|
|
31440
31522
|
updatedAt: now
|
|
31441
31523
|
}).run();
|
|
31442
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31524
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
|
|
31443
31525
|
}
|
|
31444
31526
|
writeAuditLog(app.db, {
|
|
31445
31527
|
projectId: project.id,
|
|
@@ -31493,7 +31575,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31493
31575
|
createdAt: existing?.createdAt ?? now,
|
|
31494
31576
|
updatedAt: now
|
|
31495
31577
|
});
|
|
31496
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
31578
|
+
const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
31497
31579
|
const config = { projectId, teamId, environment };
|
|
31498
31580
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
31499
31581
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -31505,10 +31587,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31505
31587
|
lastError: null,
|
|
31506
31588
|
configJson: config,
|
|
31507
31589
|
updatedAt: now
|
|
31508
|
-
}).where(
|
|
31509
|
-
row = tx.select().from(trafficSources).where(
|
|
31590
|
+
}).where(eq29(trafficSources.id, activeSource.id)).run();
|
|
31591
|
+
row = tx.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
|
|
31510
31592
|
} else {
|
|
31511
|
-
const newId =
|
|
31593
|
+
const newId = crypto26.randomUUID();
|
|
31512
31594
|
tx.insert(trafficSources).values({
|
|
31513
31595
|
id: newId,
|
|
31514
31596
|
projectId: project.id,
|
|
@@ -31531,18 +31613,18 @@ async function trafficRoutes(app, opts) {
|
|
|
31531
31613
|
createdAt: now,
|
|
31532
31614
|
updatedAt: now
|
|
31533
31615
|
}).run();
|
|
31534
|
-
row = tx.select().from(trafficSources).where(
|
|
31616
|
+
row = tx.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
|
|
31535
31617
|
}
|
|
31536
31618
|
const existingSchedule = tx.select().from(schedules).where(
|
|
31537
31619
|
and24(
|
|
31538
|
-
|
|
31539
|
-
|
|
31620
|
+
eq29(schedules.projectId, project.id),
|
|
31621
|
+
eq29(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
31540
31622
|
)
|
|
31541
31623
|
).get();
|
|
31542
31624
|
let created = false;
|
|
31543
31625
|
if (!existingSchedule) {
|
|
31544
31626
|
tx.insert(schedules).values({
|
|
31545
|
-
id:
|
|
31627
|
+
id: crypto26.randomUUID(),
|
|
31546
31628
|
projectId: project.id,
|
|
31547
31629
|
kind: SchedulableRunKinds["traffic-sync"],
|
|
31548
31630
|
cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
|
|
@@ -31585,7 +31667,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31585
31667
|
});
|
|
31586
31668
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
31587
31669
|
const project = resolveProject(app.db, request.params.name);
|
|
31588
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
31670
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
|
|
31589
31671
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
31590
31672
|
throw notFound("Traffic source", request.params.id);
|
|
31591
31673
|
}
|
|
@@ -31597,7 +31679,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31597
31679
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
31598
31680
|
const startedAt = windowEnd.toISOString();
|
|
31599
31681
|
const syncStartedAtMs = windowEnd.getTime();
|
|
31600
|
-
const runId =
|
|
31682
|
+
const runId = crypto26.randomUUID();
|
|
31601
31683
|
app.db.insert(runs).values({
|
|
31602
31684
|
id: runId,
|
|
31603
31685
|
projectId: project.id,
|
|
@@ -31611,8 +31693,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31611
31693
|
const markFailed = (msg, errorCode) => {
|
|
31612
31694
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31613
31695
|
app.db.transaction((tx) => {
|
|
31614
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
31615
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
31696
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq29(runs.id, runId)).run();
|
|
31697
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
31616
31698
|
});
|
|
31617
31699
|
try {
|
|
31618
31700
|
opts.onTrafficSynced?.({
|
|
@@ -31693,7 +31775,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31693
31775
|
}
|
|
31694
31776
|
const credential = credentialStore.getConnection(project.name);
|
|
31695
31777
|
if (!credential) {
|
|
31696
|
-
app.db.delete(runs).where(
|
|
31778
|
+
app.db.delete(runs).where(eq29(runs.id, runId)).run();
|
|
31697
31779
|
throw validationError(
|
|
31698
31780
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
31699
31781
|
);
|
|
@@ -31742,12 +31824,12 @@ async function trafficRoutes(app, opts) {
|
|
|
31742
31824
|
auditAction = "traffic.vercel.synced";
|
|
31743
31825
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
31744
31826
|
if (!credentialStore) {
|
|
31745
|
-
app.db.delete(runs).where(
|
|
31827
|
+
app.db.delete(runs).where(eq29(runs.id, runId)).run();
|
|
31746
31828
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
31747
31829
|
}
|
|
31748
31830
|
const credential = credentialStore.getConnection(project.name);
|
|
31749
31831
|
if (!credential) {
|
|
31750
|
-
app.db.delete(runs).where(
|
|
31832
|
+
app.db.delete(runs).where(eq29(runs.id, runId)).run();
|
|
31751
31833
|
throw validationError(
|
|
31752
31834
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
31753
31835
|
);
|
|
@@ -31847,7 +31929,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31847
31929
|
let aiReferralHitsCount = 0;
|
|
31848
31930
|
let unknownHitsCount = 0;
|
|
31849
31931
|
app.db.transaction((tx) => {
|
|
31850
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
31932
|
+
const latestRow = tx.select().from(trafficSources).where(eq29(trafficSources.id, sourceRow.id)).get();
|
|
31851
31933
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
31852
31934
|
const seenEventIds = new Set(previousIds);
|
|
31853
31935
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -31987,7 +32069,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31987
32069
|
}
|
|
31988
32070
|
})();
|
|
31989
32071
|
tx.insert(rawEventSamples).values({
|
|
31990
|
-
id:
|
|
32072
|
+
id: crypto26.randomUUID(),
|
|
31991
32073
|
projectId: project.id,
|
|
31992
32074
|
sourceId: sourceRow.id,
|
|
31993
32075
|
ts: sample.observedAt,
|
|
@@ -32022,8 +32104,8 @@ async function trafficRoutes(app, opts) {
|
|
|
32022
32104
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
32023
32105
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
32024
32106
|
}
|
|
32025
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
32026
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
32107
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
32108
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
|
|
32027
32109
|
});
|
|
32028
32110
|
writeAuditLog(app.db, {
|
|
32029
32111
|
projectId: project.id,
|
|
@@ -32075,7 +32157,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32075
32157
|
});
|
|
32076
32158
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
32077
32159
|
const project = resolveProject(app.db, request.params.name);
|
|
32078
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
32160
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
|
|
32079
32161
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
32080
32162
|
throw notFound("Traffic source", request.params.id);
|
|
32081
32163
|
}
|
|
@@ -32225,7 +32307,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32225
32307
|
};
|
|
32226
32308
|
}
|
|
32227
32309
|
const startedAt = windowEnd.toISOString();
|
|
32228
|
-
const runId =
|
|
32310
|
+
const runId = crypto26.randomUUID();
|
|
32229
32311
|
app.db.insert(runs).values({
|
|
32230
32312
|
id: runId,
|
|
32231
32313
|
projectId: project.id,
|
|
@@ -32264,7 +32346,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32264
32346
|
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
32265
32347
|
}).from(crawlerEventsHourly).where(
|
|
32266
32348
|
and24(
|
|
32267
|
-
|
|
32349
|
+
eq29(crawlerEventsHourly.sourceId, row.id),
|
|
32268
32350
|
gte4(crawlerEventsHourly.tsHour, since)
|
|
32269
32351
|
)
|
|
32270
32352
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
@@ -32274,27 +32356,27 @@ async function trafficRoutes(app, opts) {
|
|
|
32274
32356
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
32275
32357
|
const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
32276
32358
|
and24(
|
|
32277
|
-
|
|
32359
|
+
eq29(aiUserFetchEventsHourly.sourceId, row.id),
|
|
32278
32360
|
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
32279
32361
|
)
|
|
32280
32362
|
).get();
|
|
32281
32363
|
const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32282
32364
|
and24(
|
|
32283
|
-
|
|
32365
|
+
eq29(aiReferralEventsHourly.sourceId, row.id),
|
|
32284
32366
|
gte4(aiReferralEventsHourly.tsHour, since)
|
|
32285
32367
|
)
|
|
32286
32368
|
).get();
|
|
32287
32369
|
const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
|
|
32288
32370
|
and24(
|
|
32289
|
-
|
|
32371
|
+
eq29(rawEventSamples.sourceId, row.id),
|
|
32290
32372
|
gte4(rawEventSamples.ts, since)
|
|
32291
32373
|
)
|
|
32292
32374
|
).get();
|
|
32293
32375
|
const latestRun = app.db.select().from(runs).where(
|
|
32294
32376
|
and24(
|
|
32295
|
-
|
|
32296
|
-
|
|
32297
|
-
|
|
32377
|
+
eq29(runs.projectId, projectId),
|
|
32378
|
+
eq29(runs.kind, RunKinds["traffic-sync"]),
|
|
32379
|
+
eq29(runs.sourceId, row.id)
|
|
32298
32380
|
)
|
|
32299
32381
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
32300
32382
|
return {
|
|
@@ -32325,7 +32407,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32325
32407
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
32326
32408
|
);
|
|
32327
32409
|
}
|
|
32328
|
-
const sourceRow = app.db.select().from(trafficSources).where(and24(
|
|
32410
|
+
const sourceRow = app.db.select().from(trafficSources).where(and24(eq29(trafficSources.projectId, project.id), eq29(trafficSources.id, request.params.id))).get();
|
|
32329
32411
|
if (!sourceRow) {
|
|
32330
32412
|
throw notFound("traffic source", request.params.id);
|
|
32331
32413
|
}
|
|
@@ -32342,7 +32424,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32342
32424
|
status: TrafficSourceStatuses.connected,
|
|
32343
32425
|
lastError: null,
|
|
32344
32426
|
updatedAt: now
|
|
32345
|
-
}).where(
|
|
32427
|
+
}).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
32346
32428
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
32347
32429
|
projectId: project.id,
|
|
32348
32430
|
actor: "api",
|
|
@@ -32350,20 +32432,20 @@ async function trafficRoutes(app, opts) {
|
|
|
32350
32432
|
entityType: "traffic_source",
|
|
32351
32433
|
entityId: sourceRow.id
|
|
32352
32434
|
}));
|
|
32353
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
32435
|
+
updatedRow = tx.select().from(trafficSources).where(eq29(trafficSources.id, sourceRow.id)).get();
|
|
32354
32436
|
});
|
|
32355
32437
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
32356
32438
|
});
|
|
32357
32439
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
32358
32440
|
const project = resolveProject(app.db, request.params.name);
|
|
32359
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
32441
|
+
const rows = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
32360
32442
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
32361
32443
|
const response = { sources };
|
|
32362
32444
|
return response;
|
|
32363
32445
|
});
|
|
32364
32446
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
32365
32447
|
const project = resolveProject(app.db, request.params.name);
|
|
32366
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
32448
|
+
const rows = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
32367
32449
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
32368
32450
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
32369
32451
|
const response = { sources };
|
|
@@ -32373,7 +32455,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32373
32455
|
"/projects/:name/traffic/sources/:id",
|
|
32374
32456
|
async (request) => {
|
|
32375
32457
|
const project = resolveProject(app.db, request.params.name);
|
|
32376
|
-
const row = app.db.select().from(trafficSources).where(
|
|
32458
|
+
const row = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
|
|
32377
32459
|
if (!row || row.projectId !== project.id) {
|
|
32378
32460
|
throw notFound("Traffic source", request.params.id);
|
|
32379
32461
|
}
|
|
@@ -32425,11 +32507,11 @@ async function trafficRoutes(app, opts) {
|
|
|
32425
32507
|
let aiReferralCounts = aiReferralClassCounts(0, 0, 0);
|
|
32426
32508
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
32427
32509
|
const crawlerFilters = [
|
|
32428
|
-
|
|
32510
|
+
eq29(crawlerEventsHourly.projectId, project.id),
|
|
32429
32511
|
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
32430
32512
|
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
32431
32513
|
];
|
|
32432
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
32514
|
+
if (sourceIdParam) crawlerFilters.push(eq29(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
32433
32515
|
const crawlerWhere = and24(...crawlerFilters);
|
|
32434
32516
|
const pathTotals = app.db.select({
|
|
32435
32517
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
@@ -32457,11 +32539,11 @@ async function trafficRoutes(app, opts) {
|
|
|
32457
32539
|
}
|
|
32458
32540
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
32459
32541
|
const userFetchFilters = [
|
|
32460
|
-
|
|
32542
|
+
eq29(aiUserFetchEventsHourly.projectId, project.id),
|
|
32461
32543
|
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
32462
32544
|
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
32463
32545
|
];
|
|
32464
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
32546
|
+
if (sourceIdParam) userFetchFilters.push(eq29(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
32465
32547
|
const userFetchWhere = and24(...userFetchFilters);
|
|
32466
32548
|
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
32467
32549
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
@@ -32482,11 +32564,11 @@ async function trafficRoutes(app, opts) {
|
|
|
32482
32564
|
}
|
|
32483
32565
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
32484
32566
|
const aiFilters = [
|
|
32485
|
-
|
|
32567
|
+
eq29(aiReferralEventsHourly.projectId, project.id),
|
|
32486
32568
|
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
32487
32569
|
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
32488
32570
|
];
|
|
32489
|
-
if (sourceIdParam) aiFilters.push(
|
|
32571
|
+
if (sourceIdParam) aiFilters.push(eq29(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
32490
32572
|
const aiWhere = and24(...aiFilters);
|
|
32491
32573
|
const total = app.db.select({
|
|
32492
32574
|
total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
@@ -32541,7 +32623,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32541
32623
|
}
|
|
32542
32624
|
|
|
32543
32625
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
32544
|
-
import
|
|
32626
|
+
import crypto27 from "crypto";
|
|
32545
32627
|
import fs6 from "fs";
|
|
32546
32628
|
import path7 from "path";
|
|
32547
32629
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -32694,7 +32776,7 @@ function isInstalled(dir) {
|
|
|
32694
32776
|
}
|
|
32695
32777
|
function hashInstalledFile(filePath) {
|
|
32696
32778
|
try {
|
|
32697
|
-
return
|
|
32779
|
+
return crypto27.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
32698
32780
|
} catch {
|
|
32699
32781
|
return void 0;
|
|
32700
32782
|
}
|
|
@@ -32709,7 +32791,7 @@ function readInstalledManifest(skillDir) {
|
|
|
32709
32791
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
32710
32792
|
|
|
32711
32793
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
32712
|
-
import { and as and25, eq as
|
|
32794
|
+
import { and as and25, eq as eq30 } from "drizzle-orm";
|
|
32713
32795
|
function skippedNoProject() {
|
|
32714
32796
|
return {
|
|
32715
32797
|
status: CheckStatuses.skipped,
|
|
@@ -32725,8 +32807,8 @@ var BACKLINKS_CHECKS = [
|
|
|
32725
32807
|
title: "Backlinks source connected",
|
|
32726
32808
|
run: (ctx) => {
|
|
32727
32809
|
if (!ctx.project) return skippedNoProject();
|
|
32728
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
32729
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
32810
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq30(projects.id, ctx.project.id)).get();
|
|
32811
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
32730
32812
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
32731
32813
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
32732
32814
|
const connected = [];
|
|
@@ -32742,8 +32824,8 @@ var BACKLINKS_CHECKS = [
|
|
|
32742
32824
|
};
|
|
32743
32825
|
}
|
|
32744
32826
|
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and25(
|
|
32745
|
-
|
|
32746
|
-
|
|
32827
|
+
eq30(backlinkSummaries.projectId, ctx.project.id),
|
|
32828
|
+
eq30(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
32747
32829
|
)).limit(1).get() : false;
|
|
32748
32830
|
return {
|
|
32749
32831
|
status: CheckStatuses.ok,
|
|
@@ -32903,7 +32985,7 @@ var BING_AUTH_CHECKS = [
|
|
|
32903
32985
|
];
|
|
32904
32986
|
|
|
32905
32987
|
// ../api-routes/src/doctor/checks/content.ts
|
|
32906
|
-
import { eq as
|
|
32988
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
32907
32989
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
32908
32990
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
32909
32991
|
function skippedNoProject2() {
|
|
@@ -32916,7 +32998,7 @@ function skippedNoProject2() {
|
|
|
32916
32998
|
}
|
|
32917
32999
|
function loadProject(ctx) {
|
|
32918
33000
|
if (!ctx.project) return null;
|
|
32919
|
-
return ctx.db.select().from(projects).where(
|
|
33001
|
+
return ctx.db.select().from(projects).where(eq31(projects.id, ctx.project.id)).get() ?? null;
|
|
32920
33002
|
}
|
|
32921
33003
|
function percent(value) {
|
|
32922
33004
|
return Math.round(value * 100);
|
|
@@ -33008,7 +33090,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
33008
33090
|
);
|
|
33009
33091
|
|
|
33010
33092
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
33011
|
-
import { eq as
|
|
33093
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
33012
33094
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
33013
33095
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
33014
33096
|
var adsConnectionCheck = {
|
|
@@ -33025,7 +33107,7 @@ var adsConnectionCheck = {
|
|
|
33025
33107
|
remediation: null
|
|
33026
33108
|
};
|
|
33027
33109
|
}
|
|
33028
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
33110
|
+
const row = ctx.db.select().from(adsConnections).where(eq32(adsConnections.projectId, ctx.project.id)).get();
|
|
33029
33111
|
if (!row) {
|
|
33030
33112
|
return {
|
|
33031
33113
|
status: CheckStatuses.skipped,
|
|
@@ -33075,7 +33157,7 @@ var adsRecentSyncCheck = {
|
|
|
33075
33157
|
remediation: null
|
|
33076
33158
|
};
|
|
33077
33159
|
}
|
|
33078
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
33160
|
+
const row = ctx.db.select().from(adsConnections).where(eq32(adsConnections.projectId, ctx.project.id)).get();
|
|
33079
33161
|
if (!row) {
|
|
33080
33162
|
return {
|
|
33081
33163
|
status: CheckStatuses.skipped,
|
|
@@ -33266,7 +33348,7 @@ var ga4ConnectionCheck = {
|
|
|
33266
33348
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
33267
33349
|
|
|
33268
33350
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
33269
|
-
import { and as and26, eq as
|
|
33351
|
+
import { and as and26, eq as eq33 } from "drizzle-orm";
|
|
33270
33352
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
33271
33353
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
33272
33354
|
function skippedNoProject3() {
|
|
@@ -33499,7 +33581,7 @@ var recentSyncCheck = {
|
|
|
33499
33581
|
title: "GBP recent sync",
|
|
33500
33582
|
run: (ctx) => {
|
|
33501
33583
|
if (!ctx.project) return skippedNoProject3();
|
|
33502
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and26(
|
|
33584
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and26(eq33(gbpLocations.projectId, ctx.project.id), eq33(gbpLocations.selected, true))).all();
|
|
33503
33585
|
if (selected.length === 0) {
|
|
33504
33586
|
return {
|
|
33505
33587
|
status: CheckStatuses.skipped,
|
|
@@ -33559,7 +33641,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
33559
33641
|
);
|
|
33560
33642
|
|
|
33561
33643
|
// ../api-routes/src/doctor/checks/places.ts
|
|
33562
|
-
import { eq as
|
|
33644
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
33563
33645
|
var apiKeyCheck = {
|
|
33564
33646
|
id: "gbp.places.api-key",
|
|
33565
33647
|
category: CheckCategories.auth,
|
|
@@ -33604,7 +33686,7 @@ var apiKeyCheck = {
|
|
|
33604
33686
|
details: { tier: cfg.tier }
|
|
33605
33687
|
};
|
|
33606
33688
|
}
|
|
33607
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
33689
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq34(gbpLocations.projectId, ctx.project.id)).all();
|
|
33608
33690
|
const selected = rows.filter((r) => r.selected);
|
|
33609
33691
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
33610
33692
|
const details = {
|
|
@@ -34101,7 +34183,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
34101
34183
|
];
|
|
34102
34184
|
|
|
34103
34185
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
34104
|
-
import { and as and27, eq as
|
|
34186
|
+
import { and as and27, eq as eq35, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
|
|
34105
34187
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
34106
34188
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
34107
34189
|
function skippedNoProject5() {
|
|
@@ -34116,7 +34198,7 @@ function loadProbes(ctx) {
|
|
|
34116
34198
|
if (!ctx.project) return [];
|
|
34117
34199
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
34118
34200
|
and27(
|
|
34119
|
-
|
|
34201
|
+
eq35(trafficSources.projectId, ctx.project.id),
|
|
34120
34202
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
34121
34203
|
)
|
|
34122
34204
|
).all();
|
|
@@ -34197,7 +34279,7 @@ var recentDataCheck = {
|
|
|
34197
34279
|
const recentCrawlers = Number(
|
|
34198
34280
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
34199
34281
|
and27(
|
|
34200
|
-
|
|
34282
|
+
eq35(crawlerEventsHourly.projectId, ctx.project.id),
|
|
34201
34283
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
34202
34284
|
)
|
|
34203
34285
|
).get()?.total ?? 0
|
|
@@ -34205,7 +34287,7 @@ var recentDataCheck = {
|
|
|
34205
34287
|
const recentReferrals = Number(
|
|
34206
34288
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
34207
34289
|
and27(
|
|
34208
|
-
|
|
34290
|
+
eq35(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
34209
34291
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
34210
34292
|
)
|
|
34211
34293
|
).get()?.total ?? 0
|
|
@@ -34221,7 +34303,7 @@ var recentDataCheck = {
|
|
|
34221
34303
|
const olderCrawlers = Number(
|
|
34222
34304
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
34223
34305
|
and27(
|
|
34224
|
-
|
|
34306
|
+
eq35(crawlerEventsHourly.projectId, ctx.project.id),
|
|
34225
34307
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
34226
34308
|
)
|
|
34227
34309
|
).get()?.total ?? 0
|
|
@@ -34229,7 +34311,7 @@ var recentDataCheck = {
|
|
|
34229
34311
|
const olderReferrals = Number(
|
|
34230
34312
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
34231
34313
|
and27(
|
|
34232
|
-
|
|
34314
|
+
eq35(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
34233
34315
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
34234
34316
|
)
|
|
34235
34317
|
).get()?.total ?? 0
|
|
@@ -34644,8 +34726,8 @@ async function doctorRoutes(app, opts) {
|
|
|
34644
34726
|
}
|
|
34645
34727
|
|
|
34646
34728
|
// ../api-routes/src/discovery/routes.ts
|
|
34647
|
-
import
|
|
34648
|
-
import { and as and28, desc as desc16, eq as
|
|
34729
|
+
import crypto28 from "crypto";
|
|
34730
|
+
import { and as and28, desc as desc16, eq as eq36, gte as gte6, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
|
|
34649
34731
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
34650
34732
|
async function discoveryRoutes(app, opts) {
|
|
34651
34733
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -34680,12 +34762,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34680
34762
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
34681
34763
|
const decision = app.db.transaction((tx) => {
|
|
34682
34764
|
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and28(
|
|
34683
|
-
|
|
34684
|
-
|
|
34765
|
+
eq36(discoverySessions.projectId, project.id),
|
|
34766
|
+
eq36(discoverySessions.icpDescription, icpDescription),
|
|
34685
34767
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
34686
34768
|
// semantics, so a request with a different (or no) buyer must start
|
|
34687
34769
|
// its own session, never adopt another buyer's probes.
|
|
34688
|
-
parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) :
|
|
34770
|
+
parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) : eq36(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
34689
34771
|
// Locations are identity too: a different service-area subset seeds
|
|
34690
34772
|
// and probes a different geo, so it must never reuse another geo's
|
|
34691
34773
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -34695,12 +34777,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34695
34777
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
34696
34778
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
34697
34779
|
// upgrade, never a wrong reuse.
|
|
34698
|
-
locations.length === 0 ? or5(isNull(discoverySessions.locations),
|
|
34780
|
+
locations.length === 0 ? or5(isNull(discoverySessions.locations), eq36(discoverySessions.locations, locations)) : eq36(discoverySessions.locations, locations),
|
|
34699
34781
|
// Seed provider set is identity: a different phrasing distribution
|
|
34700
34782
|
// must never reuse another set's session. Null (= Gemini-only
|
|
34701
34783
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
34702
|
-
seedProviders == null ? isNull(discoverySessions.seedProviders) :
|
|
34703
|
-
|
|
34784
|
+
seedProviders == null ? isNull(discoverySessions.seedProviders) : eq36(discoverySessions.seedProviders, seedProviders),
|
|
34785
|
+
inArray13(discoverySessions.status, [
|
|
34704
34786
|
DiscoverySessionStatuses.queued,
|
|
34705
34787
|
DiscoverySessionStatuses.seeding,
|
|
34706
34788
|
DiscoverySessionStatuses.probing
|
|
@@ -34710,8 +34792,8 @@ async function discoveryRoutes(app, opts) {
|
|
|
34710
34792
|
if (existing && existing.runId) {
|
|
34711
34793
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
34712
34794
|
}
|
|
34713
|
-
const sessionId =
|
|
34714
|
-
const runId =
|
|
34795
|
+
const sessionId = crypto28.randomUUID();
|
|
34796
|
+
const runId = crypto28.randomUUID();
|
|
34715
34797
|
tx.insert(discoverySessions).values({
|
|
34716
34798
|
id: sessionId,
|
|
34717
34799
|
projectId: project.id,
|
|
@@ -34775,7 +34857,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34775
34857
|
const project = resolveProject(app.db, request.params.name);
|
|
34776
34858
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
34777
34859
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
34778
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
34860
|
+
const rows = app.db.select().from(discoverySessions).where(eq36(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
34779
34861
|
return reply.send(rows.map(serializeSession));
|
|
34780
34862
|
}
|
|
34781
34863
|
);
|
|
@@ -34783,11 +34865,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
34783
34865
|
"/projects/:name/discover/sessions/:id",
|
|
34784
34866
|
async (request, reply) => {
|
|
34785
34867
|
const project = resolveProject(app.db, request.params.name);
|
|
34786
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34868
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34787
34869
|
if (!session || session.projectId !== project.id) {
|
|
34788
34870
|
throw notFound("Discovery session", request.params.id);
|
|
34789
34871
|
}
|
|
34790
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34872
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34791
34873
|
const detail = {
|
|
34792
34874
|
...serializeSession(session),
|
|
34793
34875
|
probes: probeRows.map(serializeProbe)
|
|
@@ -34799,7 +34881,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34799
34881
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
34800
34882
|
async (request, reply) => {
|
|
34801
34883
|
const project = resolveProject(app.db, request.params.name);
|
|
34802
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34884
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34803
34885
|
if (!session || session.projectId !== project.id) {
|
|
34804
34886
|
throw notFound("Discovery session", request.params.id);
|
|
34805
34887
|
}
|
|
@@ -34807,7 +34889,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34807
34889
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
34808
34890
|
const applyAnchor = request.query.anchor !== "false";
|
|
34809
34891
|
const provider = session.seedProvider ?? "gemini";
|
|
34810
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34892
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34811
34893
|
const extract = opts.harvestSearchQueries;
|
|
34812
34894
|
const probesWithQueries = probeRows.map((row) => {
|
|
34813
34895
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -34818,7 +34900,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34818
34900
|
return { searchQueries: [] };
|
|
34819
34901
|
}
|
|
34820
34902
|
});
|
|
34821
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
34903
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq36(queries.projectId, project.id)).all().map((r) => r.query);
|
|
34822
34904
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
34823
34905
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
34824
34906
|
effectiveDomains(project)
|
|
@@ -34865,12 +34947,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34865
34947
|
"/projects/:name/discover/sessions/:id/promote",
|
|
34866
34948
|
async (request, reply) => {
|
|
34867
34949
|
const project = resolveProject(app.db, request.params.name);
|
|
34868
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34950
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34869
34951
|
if (!session || session.projectId !== project.id) {
|
|
34870
34952
|
throw notFound("Discovery session", request.params.id);
|
|
34871
34953
|
}
|
|
34872
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34873
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34954
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34955
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
34874
34956
|
const seenCompetitors = new Set(existingCompetitors);
|
|
34875
34957
|
const cited = /* @__PURE__ */ new Set();
|
|
34876
34958
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -34899,7 +34981,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34899
34981
|
);
|
|
34900
34982
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
34901
34983
|
const project = resolveProject(app.db, request.params.name);
|
|
34902
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34984
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34903
34985
|
if (!session || session.projectId !== project.id) {
|
|
34904
34986
|
throw notFound("Discovery session", request.params.id);
|
|
34905
34987
|
}
|
|
@@ -34922,7 +35004,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34922
35004
|
const bucketSet = new Set(buckets);
|
|
34923
35005
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
34924
35006
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
34925
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
35007
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34926
35008
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
34927
35009
|
for (const probe of probeRows) {
|
|
34928
35010
|
if (!probe.bucket) continue;
|
|
@@ -34930,7 +35012,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34930
35012
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
34931
35013
|
}
|
|
34932
35014
|
const existingQueries = new Set(
|
|
34933
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
35015
|
+
app.db.select({ query: queries.query }).from(queries).where(eq36(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
34934
35016
|
);
|
|
34935
35017
|
const promotedQueries = [];
|
|
34936
35018
|
const skippedQueries = [];
|
|
@@ -34946,7 +35028,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34946
35028
|
const skippedCompetitors = [];
|
|
34947
35029
|
if (includeCompetitors) {
|
|
34948
35030
|
const existingCompetitors = new Set(
|
|
34949
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
35031
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
34950
35032
|
);
|
|
34951
35033
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
34952
35034
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -34965,7 +35047,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34965
35047
|
app.db.transaction((tx) => {
|
|
34966
35048
|
for (const query of promotedQueries) {
|
|
34967
35049
|
tx.insert(queries).values({
|
|
34968
|
-
id:
|
|
35050
|
+
id: crypto28.randomUUID(),
|
|
34969
35051
|
projectId: project.id,
|
|
34970
35052
|
query,
|
|
34971
35053
|
provenance,
|
|
@@ -34974,7 +35056,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34974
35056
|
}
|
|
34975
35057
|
for (const domain of promotedCompetitors) {
|
|
34976
35058
|
tx.insert(competitors).values({
|
|
34977
|
-
id:
|
|
35059
|
+
id: crypto28.randomUUID(),
|
|
34978
35060
|
projectId: project.id,
|
|
34979
35061
|
domain,
|
|
34980
35062
|
provenance,
|
|
@@ -35064,8 +35146,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
35064
35146
|
}
|
|
35065
35147
|
|
|
35066
35148
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
35067
|
-
import
|
|
35068
|
-
import { eq as
|
|
35149
|
+
import crypto29 from "crypto";
|
|
35150
|
+
import { eq as eq37 } from "drizzle-orm";
|
|
35069
35151
|
var DEFAULT_MAX_PROBES = 100;
|
|
35070
35152
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
35071
35153
|
function classifyProbeBucket(input) {
|
|
@@ -35152,7 +35234,7 @@ async function executeDiscovery(opts) {
|
|
|
35152
35234
|
status: DiscoverySessionStatuses.seeding,
|
|
35153
35235
|
dedupThreshold,
|
|
35154
35236
|
startedAt
|
|
35155
|
-
}).where(
|
|
35237
|
+
}).where(eq37(discoverySessions.id, opts.sessionId)).run();
|
|
35156
35238
|
const seedResult = await opts.deps.seed({
|
|
35157
35239
|
project: opts.project,
|
|
35158
35240
|
icpDescription: opts.icpDescription,
|
|
@@ -35213,7 +35295,7 @@ async function executeDiscovery(opts) {
|
|
|
35213
35295
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
35214
35296
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
35215
35297
|
warning
|
|
35216
|
-
}).where(
|
|
35298
|
+
}).where(eq37(discoverySessions.id, opts.sessionId)).run();
|
|
35217
35299
|
const probeLocation = opts.locations?.[0];
|
|
35218
35300
|
const probeResults = await mapWithConcurrency(
|
|
35219
35301
|
probedCanonicals,
|
|
@@ -35232,7 +35314,7 @@ async function executeDiscovery(opts) {
|
|
|
35232
35314
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
35233
35315
|
buckets[bucket]++;
|
|
35234
35316
|
return {
|
|
35235
|
-
id:
|
|
35317
|
+
id: crypto29.randomUUID(),
|
|
35236
35318
|
sessionId: opts.sessionId,
|
|
35237
35319
|
projectId: opts.project.id,
|
|
35238
35320
|
query: probedCanonicals[index2],
|
|
@@ -35267,7 +35349,7 @@ async function executeDiscovery(opts) {
|
|
|
35267
35349
|
wastedCount: buckets["wasted-surface"],
|
|
35268
35350
|
competitorMap,
|
|
35269
35351
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35270
|
-
}).where(
|
|
35352
|
+
}).where(eq37(discoverySessions.id, opts.sessionId)).run();
|
|
35271
35353
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
35272
35354
|
return {
|
|
35273
35355
|
buckets,
|
|
@@ -35284,7 +35366,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
|
|
|
35284
35366
|
const domain = normalizeDomain(entry.domain);
|
|
35285
35367
|
if (!domain) continue;
|
|
35286
35368
|
db.insert(domainClassifications).values({
|
|
35287
|
-
id:
|
|
35369
|
+
id: crypto29.randomUUID(),
|
|
35288
35370
|
projectId,
|
|
35289
35371
|
domain,
|
|
35290
35372
|
competitorType: entry.competitorType,
|
|
@@ -35307,7 +35389,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
35307
35389
|
status: DiscoverySessionStatuses.failed,
|
|
35308
35390
|
error,
|
|
35309
35391
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35310
|
-
}).where(
|
|
35392
|
+
}).where(eq37(discoverySessions.id, sessionId)).run();
|
|
35311
35393
|
}
|
|
35312
35394
|
function dedupeStrings(input) {
|
|
35313
35395
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -35324,8 +35406,8 @@ function dedupeStrings(input) {
|
|
|
35324
35406
|
}
|
|
35325
35407
|
|
|
35326
35408
|
// ../api-routes/src/technical-aeo.ts
|
|
35327
|
-
import
|
|
35328
|
-
import { and as and29, asc as asc5, count, desc as desc17, eq as
|
|
35409
|
+
import crypto30 from "crypto";
|
|
35410
|
+
import { and as and29, asc as asc5, count, desc as desc17, eq as eq38, inArray as inArray14 } from "drizzle-orm";
|
|
35329
35411
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
35330
35412
|
function emptyScore(projectName) {
|
|
35331
35413
|
return {
|
|
@@ -35357,10 +35439,10 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
35357
35439
|
async function technicalAeoRoutes(app, opts) {
|
|
35358
35440
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
35359
35441
|
const project = resolveProject(app.db, request.params.name);
|
|
35360
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
35361
|
-
|
|
35362
|
-
|
|
35363
|
-
|
|
35442
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
|
|
35443
|
+
eq38(siteAuditSnapshots.projectId, project.id),
|
|
35444
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35445
|
+
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35364
35446
|
notProbeRun()
|
|
35365
35447
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
35366
35448
|
const latest = rows[0];
|
|
@@ -35392,18 +35474,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35392
35474
|
});
|
|
35393
35475
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
35394
35476
|
const project = resolveProject(app.db, request.params.name);
|
|
35395
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
35396
|
-
|
|
35397
|
-
|
|
35398
|
-
|
|
35477
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
|
|
35478
|
+
eq38(siteAuditSnapshots.projectId, project.id),
|
|
35479
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35480
|
+
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35399
35481
|
notProbeRun()
|
|
35400
35482
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
35401
35483
|
if (!latest) {
|
|
35402
35484
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
35403
35485
|
}
|
|
35404
35486
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
35405
|
-
const conds = [
|
|
35406
|
-
if (statusFilter) conds.push(
|
|
35487
|
+
const conds = [eq38(siteAuditPages.runId, latest.runId)];
|
|
35488
|
+
if (statusFilter) conds.push(eq38(siteAuditPages.status, statusFilter));
|
|
35407
35489
|
const where = and29(...conds);
|
|
35408
35490
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
35409
35491
|
const total = totalRow?.value ?? 0;
|
|
@@ -35428,10 +35510,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35428
35510
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
35429
35511
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
35430
35512
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
35431
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
35432
|
-
|
|
35433
|
-
|
|
35434
|
-
|
|
35513
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
|
|
35514
|
+
eq38(siteAuditSnapshots.projectId, project.id),
|
|
35515
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35516
|
+
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35435
35517
|
notProbeRun()
|
|
35436
35518
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
35437
35519
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -35443,15 +35525,15 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35443
35525
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
35444
35526
|
}
|
|
35445
35527
|
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and29(
|
|
35446
|
-
|
|
35447
|
-
|
|
35448
|
-
|
|
35528
|
+
eq38(runs.projectId, project.id),
|
|
35529
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35530
|
+
inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
35449
35531
|
)).get();
|
|
35450
35532
|
if (existing) {
|
|
35451
35533
|
return { runId: existing.id, status: existing.status };
|
|
35452
35534
|
}
|
|
35453
35535
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
35454
|
-
const runId =
|
|
35536
|
+
const runId = crypto30.randomUUID();
|
|
35455
35537
|
app.db.insert(runs).values({
|
|
35456
35538
|
id: runId,
|
|
35457
35539
|
projectId: project.id,
|
|
@@ -35804,7 +35886,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
35804
35886
|
}
|
|
35805
35887
|
|
|
35806
35888
|
// src/intelligence-service.ts
|
|
35807
|
-
import
|
|
35889
|
+
import crypto31 from "crypto";
|
|
35808
35890
|
|
|
35809
35891
|
// src/logger.ts
|
|
35810
35892
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -36037,8 +36119,8 @@ var IntelligenceService = class {
|
|
|
36037
36119
|
analyzeAndPersist(runId, projectId) {
|
|
36038
36120
|
const recentRuns = this.db.select().from(runs).where(
|
|
36039
36121
|
and30(
|
|
36040
|
-
|
|
36041
|
-
or6(
|
|
36122
|
+
eq39(runs.projectId, projectId),
|
|
36123
|
+
or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
|
|
36042
36124
|
// Defensive: RunCoordinator already skips probes before this is
|
|
36043
36125
|
// called, but if a future call site invokes analyzeAndPersist
|
|
36044
36126
|
// directly for a probe, probes still must not pollute the
|
|
@@ -36120,7 +36202,7 @@ var IntelligenceService = class {
|
|
|
36120
36202
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
36121
36203
|
*/
|
|
36122
36204
|
analyzeAndPersistGbp(runId, projectId) {
|
|
36123
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
36205
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq39(runs.id, runId)).get();
|
|
36124
36206
|
if (!runRow) {
|
|
36125
36207
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
36126
36208
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -36129,8 +36211,8 @@ var IntelligenceService = class {
|
|
|
36129
36211
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
36130
36212
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
36131
36213
|
const selected = this.db.select().from(gbpLocations).where(and30(
|
|
36132
|
-
|
|
36133
|
-
|
|
36214
|
+
eq39(gbpLocations.projectId, projectId),
|
|
36215
|
+
eq39(gbpLocations.selected, true),
|
|
36134
36216
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
36135
36217
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
36136
36218
|
)).all();
|
|
@@ -36165,12 +36247,12 @@ var IntelligenceService = class {
|
|
|
36165
36247
|
}
|
|
36166
36248
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
36167
36249
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
36168
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and30(
|
|
36169
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and30(
|
|
36170
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and30(
|
|
36171
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and30(
|
|
36250
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and30(eq39(gbpDailyMetrics.projectId, projectId), eq39(gbpDailyMetrics.locationName, locationName))).all();
|
|
36251
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and30(eq39(gbpPlaceActions.projectId, projectId), eq39(gbpPlaceActions.locationName, locationName))).all();
|
|
36252
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and30(eq39(gbpLodgingSnapshots.projectId, projectId), eq39(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
36253
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and30(eq39(gbpLocations.projectId, projectId), eq39(gbpLocations.locationName, locationName))).get();
|
|
36172
36254
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
36173
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and30(
|
|
36255
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and30(eq39(gbpPlaceDetails.projectId, projectId), eq39(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
36174
36256
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
36175
36257
|
const summary = buildGbpSummary({
|
|
36176
36258
|
locationName,
|
|
@@ -36206,7 +36288,7 @@ var IntelligenceService = class {
|
|
|
36206
36288
|
/** Build the month-over-month keyword series for a location from the
|
|
36207
36289
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
36208
36290
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
36209
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and30(
|
|
36291
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and30(eq39(gbpKeywordMonthly.projectId, projectId), eq39(gbpKeywordMonthly.locationName, locationName))).all();
|
|
36210
36292
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
36211
36293
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
36212
36294
|
const recentMonth = months[0] ?? null;
|
|
@@ -36237,7 +36319,7 @@ var IntelligenceService = class {
|
|
|
36237
36319
|
*/
|
|
36238
36320
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
36239
36321
|
const covered = new Set(coveredLocationNames);
|
|
36240
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and30(
|
|
36322
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and30(eq39(insights.projectId, projectId), eq39(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
36241
36323
|
const staleIds = [];
|
|
36242
36324
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
36243
36325
|
for (const row of existing) {
|
|
@@ -36248,7 +36330,7 @@ var IntelligenceService = class {
|
|
|
36248
36330
|
}
|
|
36249
36331
|
this.db.transaction((tx) => {
|
|
36250
36332
|
for (const id of staleIds) {
|
|
36251
|
-
tx.delete(insights).where(
|
|
36333
|
+
tx.delete(insights).where(eq39(insights.id, id)).run();
|
|
36252
36334
|
}
|
|
36253
36335
|
for (const insight of gbpInsights) {
|
|
36254
36336
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -36326,7 +36408,7 @@ var IntelligenceService = class {
|
|
|
36326
36408
|
* create per run + aggregate). DB is left untouched.
|
|
36327
36409
|
*/
|
|
36328
36410
|
backfill(projectName, opts, onProgress) {
|
|
36329
|
-
const project = this.db.select().from(projects).where(
|
|
36411
|
+
const project = this.db.select().from(projects).where(eq39(projects.name, projectName)).get();
|
|
36330
36412
|
if (!project) {
|
|
36331
36413
|
throw new Error(`Project "${projectName}" not found`);
|
|
36332
36414
|
}
|
|
@@ -36340,8 +36422,8 @@ var IntelligenceService = class {
|
|
|
36340
36422
|
}
|
|
36341
36423
|
const allRuns = this.db.select().from(runs).where(
|
|
36342
36424
|
and30(
|
|
36343
|
-
|
|
36344
|
-
or6(
|
|
36425
|
+
eq39(runs.projectId, project.id),
|
|
36426
|
+
or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
|
|
36345
36427
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
36346
36428
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36347
36429
|
)
|
|
@@ -36374,7 +36456,7 @@ var IntelligenceService = class {
|
|
|
36374
36456
|
let wouldDeleteTotal = 0;
|
|
36375
36457
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
36376
36458
|
if (isDryRun && targetRuns.length > 0) {
|
|
36377
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
36459
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray15(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
36378
36460
|
for (const r of rows) {
|
|
36379
36461
|
if (r.runId == null) continue;
|
|
36380
36462
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -36420,7 +36502,7 @@ var IntelligenceService = class {
|
|
|
36420
36502
|
return { processed, skipped, totalInsights };
|
|
36421
36503
|
}
|
|
36422
36504
|
loadTrackedCompetitors(projectId) {
|
|
36423
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
36505
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
36424
36506
|
}
|
|
36425
36507
|
/**
|
|
36426
36508
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -36441,15 +36523,15 @@ var IntelligenceService = class {
|
|
|
36441
36523
|
}
|
|
36442
36524
|
persistResult(result, runId, projectId) {
|
|
36443
36525
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
36444
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
36526
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq39(insights.runId, runId)).all();
|
|
36445
36527
|
for (const row of existingInsights) {
|
|
36446
36528
|
if (row.dismissed) {
|
|
36447
36529
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
36448
36530
|
}
|
|
36449
36531
|
}
|
|
36450
36532
|
this.db.transaction((tx) => {
|
|
36451
|
-
tx.delete(insights).where(
|
|
36452
|
-
tx.delete(healthSnapshots).where(
|
|
36533
|
+
tx.delete(insights).where(eq39(insights.runId, runId)).run();
|
|
36534
|
+
tx.delete(healthSnapshots).where(eq39(healthSnapshots.runId, runId)).run();
|
|
36453
36535
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36454
36536
|
for (const insight of result.insights) {
|
|
36455
36537
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -36469,7 +36551,7 @@ var IntelligenceService = class {
|
|
|
36469
36551
|
}).run();
|
|
36470
36552
|
}
|
|
36471
36553
|
tx.insert(healthSnapshots).values({
|
|
36472
|
-
id:
|
|
36554
|
+
id: crypto31.randomUUID(),
|
|
36473
36555
|
projectId,
|
|
36474
36556
|
runId,
|
|
36475
36557
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -36502,14 +36584,14 @@ var IntelligenceService = class {
|
|
|
36502
36584
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
36503
36585
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
36504
36586
|
if (regressions.length === 0) return rawInsights;
|
|
36505
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
36587
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq39(gscSearchData.projectId, projectId)).all();
|
|
36506
36588
|
const gscConnected = gscRows.length > 0;
|
|
36507
36589
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
36508
36590
|
for (const row of gscRows) {
|
|
36509
36591
|
const key = row.query.toLowerCase();
|
|
36510
36592
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
36511
36593
|
}
|
|
36512
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
36594
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq39(projects.id, projectId)).get();
|
|
36513
36595
|
const locationCount = Math.max(
|
|
36514
36596
|
1,
|
|
36515
36597
|
(projectRow?.locations ?? []).length
|
|
@@ -36517,9 +36599,9 @@ var IntelligenceService = class {
|
|
|
36517
36599
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
36518
36600
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
36519
36601
|
and30(
|
|
36520
|
-
|
|
36521
|
-
|
|
36522
|
-
or6(
|
|
36602
|
+
eq39(runs.projectId, projectId),
|
|
36603
|
+
eq39(runs.kind, RunKinds["answer-visibility"]),
|
|
36604
|
+
or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
|
|
36523
36605
|
// Defensive — see top of file.
|
|
36524
36606
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36525
36607
|
)
|
|
@@ -36539,7 +36621,7 @@ var IntelligenceService = class {
|
|
|
36539
36621
|
const haveHistory = recentRunIds.length > 0;
|
|
36540
36622
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
36541
36623
|
if (haveHistory) {
|
|
36542
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and30(
|
|
36624
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and30(eq39(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
|
|
36543
36625
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
36544
36626
|
for (const row of priorRows) {
|
|
36545
36627
|
if (!row.runId) continue;
|
|
@@ -36568,7 +36650,7 @@ var IntelligenceService = class {
|
|
|
36568
36650
|
});
|
|
36569
36651
|
}
|
|
36570
36652
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
36571
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
36653
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq39(projects.id, projectId)).get();
|
|
36572
36654
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
36573
36655
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
36574
36656
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -36585,7 +36667,7 @@ var IntelligenceService = class {
|
|
|
36585
36667
|
citedDomains: querySnapshots.citedDomains,
|
|
36586
36668
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
36587
36669
|
snapshotLocation: querySnapshots.location
|
|
36588
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
36670
|
+
}).from(querySnapshots).leftJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(eq39(querySnapshots.runId, runId)).all();
|
|
36589
36671
|
const snapshots = [];
|
|
36590
36672
|
let orphanCount = 0;
|
|
36591
36673
|
for (const r of rows) {
|