@ainyc/canonry 1.30.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LMSO32GF.js → chunk-YW4IZ34Z.js} +305 -551
- package/dist/cli.js +4 -5
- package/dist/index.js +1 -1
- package/package.json +7 -7
|
@@ -494,6 +494,9 @@ function unsupportedKind(kind) {
|
|
|
494
494
|
function notImplemented(message) {
|
|
495
495
|
return new AppError("NOT_IMPLEMENTED", message, 501);
|
|
496
496
|
}
|
|
497
|
+
function deliveryFailed(message) {
|
|
498
|
+
return new AppError("DELIVERY_FAILED", message, 502);
|
|
499
|
+
}
|
|
497
500
|
|
|
498
501
|
// ../contracts/src/google.ts
|
|
499
502
|
import { z as z5 } from "zod";
|
|
@@ -1406,6 +1409,16 @@ function createClient(databasePath) {
|
|
|
1406
1409
|
return drizzle(sqlite, { schema: schema_exports });
|
|
1407
1410
|
}
|
|
1408
1411
|
|
|
1412
|
+
// ../db/src/json.ts
|
|
1413
|
+
function parseJsonColumn(value, fallback) {
|
|
1414
|
+
if (value == null || value === "") return fallback;
|
|
1415
|
+
try {
|
|
1416
|
+
return JSON.parse(value);
|
|
1417
|
+
} catch {
|
|
1418
|
+
return fallback;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1409
1422
|
// ../db/src/migrate.ts
|
|
1410
1423
|
import { sql } from "drizzle-orm";
|
|
1411
1424
|
var MIGRATION_SQL = `
|
|
@@ -1806,7 +1819,7 @@ import { eq as eq3 } from "drizzle-orm";
|
|
|
1806
1819
|
|
|
1807
1820
|
// ../api-routes/src/helpers.ts
|
|
1808
1821
|
import crypto3 from "crypto";
|
|
1809
|
-
import { eq as eq2,
|
|
1822
|
+
import { eq as eq2, sql as sql2 } from "drizzle-orm";
|
|
1810
1823
|
function resolveProject(db, name) {
|
|
1811
1824
|
const project = db.select().from(projects).where(eq2(projects.name, name)).get();
|
|
1812
1825
|
if (!project) {
|
|
@@ -1834,43 +1847,39 @@ async function projectRoutes(app, opts) {
|
|
|
1834
1847
|
const { name } = request.params;
|
|
1835
1848
|
const parsedBody = projectUpsertRequestSchema.safeParse(request.body);
|
|
1836
1849
|
if (!parsedBody.success) {
|
|
1837
|
-
|
|
1850
|
+
throw validationError("Invalid project payload", {
|
|
1838
1851
|
issues: parsedBody.error.issues.map((issue) => ({
|
|
1839
1852
|
path: issue.path.join("."),
|
|
1840
1853
|
message: issue.message
|
|
1841
1854
|
}))
|
|
1842
1855
|
});
|
|
1843
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1844
1856
|
}
|
|
1845
1857
|
const body = parsedBody.data;
|
|
1846
1858
|
const validNames = opts.validProviderNames ?? [];
|
|
1847
1859
|
if (validNames.length && body.providers?.length) {
|
|
1848
1860
|
const invalid = body.providers.filter((p) => !validNames.includes(p));
|
|
1849
1861
|
if (invalid.length) {
|
|
1850
|
-
|
|
1862
|
+
throw validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1851
1863
|
invalidProviders: invalid,
|
|
1852
1864
|
validProviders: validNames
|
|
1853
1865
|
});
|
|
1854
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1855
1866
|
}
|
|
1856
1867
|
}
|
|
1857
1868
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1858
1869
|
const existing = app.db.select().from(projects).where(eq3(projects.name, name)).get();
|
|
1859
|
-
const existingLocations = existing ?
|
|
1870
|
+
const existingLocations = existing ? parseJsonColumn(existing.locations, []) : [];
|
|
1860
1871
|
const nextLocations = body.locations ?? existingLocations;
|
|
1861
1872
|
const duplicateLabels = findDuplicateLocationLabels(nextLocations);
|
|
1862
1873
|
if (duplicateLabels.length > 0) {
|
|
1863
|
-
|
|
1874
|
+
throw validationError(`Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`, {
|
|
1864
1875
|
duplicateLabels
|
|
1865
1876
|
});
|
|
1866
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1867
1877
|
}
|
|
1868
1878
|
const nextDefaultLocation = body.defaultLocation !== void 0 ? body.defaultLocation ?? null : existing?.defaultLocation ?? null;
|
|
1869
1879
|
if (!hasLocationLabel(nextLocations, nextDefaultLocation)) {
|
|
1870
|
-
|
|
1880
|
+
throw validationError(`defaultLocation "${nextDefaultLocation}" must match a configured location label`, {
|
|
1871
1881
|
defaultLocation: nextDefaultLocation
|
|
1872
1882
|
});
|
|
1873
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1874
1883
|
}
|
|
1875
1884
|
if (existing) {
|
|
1876
1885
|
app.db.update(projects).set({
|
|
@@ -1932,28 +1941,11 @@ async function projectRoutes(app, opts) {
|
|
|
1932
1941
|
return reply.send(rows.map(formatProject));
|
|
1933
1942
|
});
|
|
1934
1943
|
app.get("/projects/:name", async (request, reply) => {
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
return reply.send(formatProject(project));
|
|
1938
|
-
} catch (e) {
|
|
1939
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1940
|
-
const err = e;
|
|
1941
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1942
|
-
}
|
|
1943
|
-
throw e;
|
|
1944
|
-
}
|
|
1944
|
+
const project = resolveProject(app.db, request.params.name);
|
|
1945
|
+
return reply.send(formatProject(project));
|
|
1945
1946
|
});
|
|
1946
1947
|
app.delete("/projects/:name", async (request, reply) => {
|
|
1947
|
-
|
|
1948
|
-
try {
|
|
1949
|
-
project = resolveProject(app.db, request.params.name);
|
|
1950
|
-
} catch (e) {
|
|
1951
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1952
|
-
const err = e;
|
|
1953
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1954
|
-
}
|
|
1955
|
-
throw e;
|
|
1956
|
-
}
|
|
1948
|
+
const project = resolveProject(app.db, request.params.name);
|
|
1957
1949
|
writeAuditLog(app.db, {
|
|
1958
1950
|
projectId: project.id,
|
|
1959
1951
|
actor: "api",
|
|
@@ -1966,26 +1958,15 @@ async function projectRoutes(app, opts) {
|
|
|
1966
1958
|
return reply.status(204).send();
|
|
1967
1959
|
});
|
|
1968
1960
|
app.post("/projects/:name/locations", async (request, reply) => {
|
|
1969
|
-
|
|
1970
|
-
try {
|
|
1971
|
-
project = resolveProject(app.db, request.params.name);
|
|
1972
|
-
} catch (e) {
|
|
1973
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1974
|
-
const err = e;
|
|
1975
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1976
|
-
}
|
|
1977
|
-
throw e;
|
|
1978
|
-
}
|
|
1961
|
+
const project = resolveProject(app.db, request.params.name);
|
|
1979
1962
|
const parsed = locationContextSchema.safeParse(request.body);
|
|
1980
1963
|
if (!parsed.success) {
|
|
1981
|
-
|
|
1982
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1964
|
+
throw validationError(parsed.error.issues.map((i) => i.message).join(", "));
|
|
1983
1965
|
}
|
|
1984
1966
|
const location = parsed.data;
|
|
1985
|
-
const existing =
|
|
1967
|
+
const existing = parseJsonColumn(project.locations, []);
|
|
1986
1968
|
if (existing.some((l) => l.label === location.label)) {
|
|
1987
|
-
|
|
1988
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
1969
|
+
throw validationError(`Location "${location.label}" already exists`);
|
|
1989
1970
|
}
|
|
1990
1971
|
existing.push(location);
|
|
1991
1972
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2003,39 +1984,20 @@ async function projectRoutes(app, opts) {
|
|
|
2003
1984
|
return reply.status(201).send(location);
|
|
2004
1985
|
});
|
|
2005
1986
|
app.get("/projects/:name/locations", async (request, reply) => {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
project = resolveProject(app.db, request.params.name);
|
|
2009
|
-
} catch (e) {
|
|
2010
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2011
|
-
const err = e;
|
|
2012
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2013
|
-
}
|
|
2014
|
-
throw e;
|
|
2015
|
-
}
|
|
2016
|
-
const locations = JSON.parse(project.locations || "[]");
|
|
1987
|
+
const project = resolveProject(app.db, request.params.name);
|
|
1988
|
+
const locations = parseJsonColumn(project.locations, []);
|
|
2017
1989
|
return reply.send({
|
|
2018
1990
|
locations,
|
|
2019
1991
|
defaultLocation: project.defaultLocation
|
|
2020
1992
|
});
|
|
2021
1993
|
});
|
|
2022
1994
|
app.delete("/projects/:name/locations/:label", async (request, reply) => {
|
|
2023
|
-
|
|
2024
|
-
try {
|
|
2025
|
-
project = resolveProject(app.db, request.params.name);
|
|
2026
|
-
} catch (e) {
|
|
2027
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2028
|
-
const err = e;
|
|
2029
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2030
|
-
}
|
|
2031
|
-
throw e;
|
|
2032
|
-
}
|
|
1995
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2033
1996
|
const label = decodeURIComponent(request.params.label);
|
|
2034
|
-
const existing =
|
|
1997
|
+
const existing = parseJsonColumn(project.locations, []);
|
|
2035
1998
|
const filtered = existing.filter((l) => l.label !== label);
|
|
2036
1999
|
if (filtered.length === existing.length) {
|
|
2037
|
-
|
|
2038
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2000
|
+
throw validationError(`Location "${label}" not found`);
|
|
2039
2001
|
}
|
|
2040
2002
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2041
2003
|
const updates = {
|
|
@@ -2056,25 +2018,14 @@ async function projectRoutes(app, opts) {
|
|
|
2056
2018
|
return reply.status(204).send();
|
|
2057
2019
|
});
|
|
2058
2020
|
app.put("/projects/:name/locations/default", async (request, reply) => {
|
|
2059
|
-
|
|
2060
|
-
try {
|
|
2061
|
-
project = resolveProject(app.db, request.params.name);
|
|
2062
|
-
} catch (e) {
|
|
2063
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2064
|
-
const err = e;
|
|
2065
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2066
|
-
}
|
|
2067
|
-
throw e;
|
|
2068
|
-
}
|
|
2021
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2069
2022
|
const label = request.body?.label;
|
|
2070
2023
|
if (!label) {
|
|
2071
|
-
|
|
2072
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2024
|
+
throw validationError("label is required");
|
|
2073
2025
|
}
|
|
2074
|
-
const existing =
|
|
2026
|
+
const existing = parseJsonColumn(project.locations, []);
|
|
2075
2027
|
if (!existing.some((l) => l.label === label)) {
|
|
2076
|
-
|
|
2077
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2028
|
+
throw validationError(`Location "${label}" not found. Add it first.`);
|
|
2078
2029
|
}
|
|
2079
2030
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2080
2031
|
app.db.update(projects).set({
|
|
@@ -2091,16 +2042,7 @@ async function projectRoutes(app, opts) {
|
|
|
2091
2042
|
return reply.send({ defaultLocation: label });
|
|
2092
2043
|
});
|
|
2093
2044
|
app.get("/projects/:name/export", async (request, reply) => {
|
|
2094
|
-
|
|
2095
|
-
try {
|
|
2096
|
-
project = resolveProject(app.db, request.params.name);
|
|
2097
|
-
} catch (e) {
|
|
2098
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2099
|
-
const err = e;
|
|
2100
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2101
|
-
}
|
|
2102
|
-
throw e;
|
|
2103
|
-
}
|
|
2045
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2104
2046
|
const kws = app.db.select().from(keywords).where(eq3(keywords.projectId, project.id)).all();
|
|
2105
2047
|
const comps = app.db.select().from(competitors).where(eq3(competitors.projectId, project.id)).all();
|
|
2106
2048
|
const schedule = app.db.select().from(schedules).where(eq3(schedules.projectId, project.id)).get();
|
|
@@ -2110,21 +2052,21 @@ async function projectRoutes(app, opts) {
|
|
|
2110
2052
|
kind: "Project",
|
|
2111
2053
|
metadata: {
|
|
2112
2054
|
name: project.name,
|
|
2113
|
-
labels:
|
|
2055
|
+
labels: parseJsonColumn(project.labels, {})
|
|
2114
2056
|
},
|
|
2115
2057
|
spec: {
|
|
2116
2058
|
displayName: project.displayName,
|
|
2117
2059
|
canonicalDomain: project.canonicalDomain,
|
|
2118
|
-
ownedDomains:
|
|
2060
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, []),
|
|
2119
2061
|
country: project.country,
|
|
2120
2062
|
language: project.language,
|
|
2121
2063
|
keywords: kws.map((k) => k.keyword),
|
|
2122
2064
|
competitors: comps.map((c) => c.domain),
|
|
2123
|
-
providers:
|
|
2124
|
-
locations:
|
|
2065
|
+
providers: parseJsonColumn(project.providers, []),
|
|
2066
|
+
locations: parseJsonColumn(project.locations, []),
|
|
2125
2067
|
...project.defaultLocation ? { defaultLocation: project.defaultLocation } : {},
|
|
2126
2068
|
notifications: notificationRows.map((row) => {
|
|
2127
|
-
const cfg =
|
|
2069
|
+
const cfg = parseJsonColumn(row.config, { url: "", events: [] });
|
|
2128
2070
|
return {
|
|
2129
2071
|
channel: row.channel,
|
|
2130
2072
|
url: cfg.url,
|
|
@@ -2135,7 +2077,7 @@ async function projectRoutes(app, opts) {
|
|
|
2135
2077
|
schedule: {
|
|
2136
2078
|
...schedule.preset ? { preset: schedule.preset } : { cron: schedule.cronExpr },
|
|
2137
2079
|
timezone: schedule.timezone,
|
|
2138
|
-
providers:
|
|
2080
|
+
providers: parseJsonColumn(schedule.providers, [])
|
|
2139
2081
|
}
|
|
2140
2082
|
} : {}
|
|
2141
2083
|
}
|
|
@@ -2149,13 +2091,13 @@ function formatProject(row) {
|
|
|
2149
2091
|
name: row.name,
|
|
2150
2092
|
displayName: row.displayName,
|
|
2151
2093
|
canonicalDomain: row.canonicalDomain,
|
|
2152
|
-
ownedDomains:
|
|
2094
|
+
ownedDomains: parseJsonColumn(row.ownedDomains, []),
|
|
2153
2095
|
country: row.country,
|
|
2154
2096
|
language: row.language,
|
|
2155
|
-
tags:
|
|
2156
|
-
labels:
|
|
2157
|
-
providers:
|
|
2158
|
-
locations:
|
|
2097
|
+
tags: parseJsonColumn(row.tags, []),
|
|
2098
|
+
labels: parseJsonColumn(row.labels, {}),
|
|
2099
|
+
providers: parseJsonColumn(row.providers, []),
|
|
2100
|
+
locations: parseJsonColumn(row.locations, []),
|
|
2159
2101
|
defaultLocation: row.defaultLocation,
|
|
2160
2102
|
configSource: row.configSource,
|
|
2161
2103
|
configRevision: row.configRevision,
|
|
@@ -2169,18 +2111,15 @@ import crypto5 from "crypto";
|
|
|
2169
2111
|
import { eq as eq4 } from "drizzle-orm";
|
|
2170
2112
|
async function keywordRoutes(app, opts) {
|
|
2171
2113
|
app.get("/projects/:name/keywords", async (request, reply) => {
|
|
2172
|
-
const project =
|
|
2173
|
-
if (!project) return;
|
|
2114
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2174
2115
|
const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
2175
2116
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
|
|
2176
2117
|
});
|
|
2177
2118
|
app.put("/projects/:name/keywords", async (request, reply) => {
|
|
2178
|
-
const project =
|
|
2179
|
-
if (!project) return;
|
|
2119
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2180
2120
|
const body = request.body;
|
|
2181
2121
|
if (!body || !Array.isArray(body.keywords)) {
|
|
2182
|
-
|
|
2183
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2122
|
+
throw validationError('Body must contain a "keywords" array');
|
|
2184
2123
|
}
|
|
2185
2124
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2186
2125
|
app.db.transaction((tx) => {
|
|
@@ -2205,12 +2144,10 @@ async function keywordRoutes(app, opts) {
|
|
|
2205
2144
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
|
|
2206
2145
|
});
|
|
2207
2146
|
app.delete("/projects/:name/keywords", async (request, reply) => {
|
|
2208
|
-
const project =
|
|
2209
|
-
if (!project) return;
|
|
2147
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2210
2148
|
const body = request.body;
|
|
2211
2149
|
if (!body || !Array.isArray(body.keywords) || body.keywords.length === 0) {
|
|
2212
|
-
|
|
2213
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2150
|
+
throw validationError('Body must contain a non-empty "keywords" array');
|
|
2214
2151
|
}
|
|
2215
2152
|
const existing = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
2216
2153
|
const toDelete = new Set(body.keywords);
|
|
@@ -2233,12 +2170,10 @@ async function keywordRoutes(app, opts) {
|
|
|
2233
2170
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
|
|
2234
2171
|
});
|
|
2235
2172
|
app.post("/projects/:name/keywords", async (request, reply) => {
|
|
2236
|
-
const project =
|
|
2237
|
-
if (!project) return;
|
|
2173
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2238
2174
|
const body = request.body;
|
|
2239
2175
|
if (!body || !Array.isArray(body.keywords)) {
|
|
2240
|
-
|
|
2241
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2176
|
+
throw validationError('Body must contain a "keywords" array');
|
|
2242
2177
|
}
|
|
2243
2178
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2244
2179
|
const existing = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
@@ -2269,30 +2204,25 @@ async function keywordRoutes(app, opts) {
|
|
|
2269
2204
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
|
|
2270
2205
|
});
|
|
2271
2206
|
app.post("/projects/:name/keywords/generate", async (request, reply) => {
|
|
2272
|
-
const project =
|
|
2273
|
-
if (!project) return;
|
|
2207
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2274
2208
|
const body = request.body;
|
|
2275
2209
|
if (!body?.provider || typeof body.provider !== "string") {
|
|
2276
|
-
|
|
2277
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2210
|
+
throw validationError('Body must contain a "provider" string');
|
|
2278
2211
|
}
|
|
2279
2212
|
const provider = body.provider.trim().toLowerCase();
|
|
2280
2213
|
const validNames = opts.validProviderNames ?? [];
|
|
2281
2214
|
if (validNames.length && !validNames.includes(provider)) {
|
|
2282
|
-
|
|
2215
|
+
throw validationError(`Unknown provider "${body.provider}". Valid providers: ${validNames.join(", ")}`, {
|
|
2283
2216
|
provider: body.provider,
|
|
2284
2217
|
validProviders: validNames
|
|
2285
2218
|
});
|
|
2286
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2287
2219
|
}
|
|
2288
2220
|
if (body.count !== void 0 && (typeof body.count !== "number" || !Number.isFinite(body.count) || !Number.isInteger(body.count))) {
|
|
2289
|
-
|
|
2290
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2221
|
+
throw validationError('"count" must be an integer');
|
|
2291
2222
|
}
|
|
2292
2223
|
const count = Math.min(Math.max(body.count ?? 5, 1), 20);
|
|
2293
2224
|
if (!opts.onGenerateKeywords) {
|
|
2294
|
-
|
|
2295
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2225
|
+
throw notImplemented("Key phrase generation is not supported in this deployment");
|
|
2296
2226
|
}
|
|
2297
2227
|
const existingRows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
2298
2228
|
const existingKeywords = existingRows.map((r) => r.keyword);
|
|
@@ -2316,36 +2246,21 @@ async function keywordRoutes(app, opts) {
|
|
|
2316
2246
|
}
|
|
2317
2247
|
});
|
|
2318
2248
|
}
|
|
2319
|
-
function resolveProjectSafe(app, name, reply) {
|
|
2320
|
-
try {
|
|
2321
|
-
return resolveProject(app.db, name);
|
|
2322
|
-
} catch (e) {
|
|
2323
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2324
|
-
const err = e;
|
|
2325
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
2326
|
-
return null;
|
|
2327
|
-
}
|
|
2328
|
-
throw e;
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
2249
|
|
|
2332
2250
|
// ../api-routes/src/competitors.ts
|
|
2333
2251
|
import crypto6 from "crypto";
|
|
2334
2252
|
import { eq as eq5 } from "drizzle-orm";
|
|
2335
2253
|
async function competitorRoutes(app) {
|
|
2336
2254
|
app.get("/projects/:name/competitors", async (request, reply) => {
|
|
2337
|
-
const project =
|
|
2338
|
-
if (!project) return;
|
|
2255
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2339
2256
|
const rows = app.db.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
|
|
2340
2257
|
return reply.send(rows.map((r) => ({ id: r.id, domain: r.domain, createdAt: r.createdAt })));
|
|
2341
2258
|
});
|
|
2342
2259
|
app.put("/projects/:name/competitors", async (request, reply) => {
|
|
2343
|
-
const project =
|
|
2344
|
-
if (!project) return;
|
|
2260
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2345
2261
|
const body = request.body;
|
|
2346
2262
|
if (!body || !Array.isArray(body.competitors)) {
|
|
2347
|
-
|
|
2348
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2263
|
+
throw validationError('Body must contain a "competitors" array');
|
|
2349
2264
|
}
|
|
2350
2265
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2351
2266
|
app.db.transaction((tx) => {
|
|
@@ -2370,18 +2285,6 @@ async function competitorRoutes(app) {
|
|
|
2370
2285
|
return reply.send(rows.map((r) => ({ id: r.id, domain: r.domain, createdAt: r.createdAt })));
|
|
2371
2286
|
});
|
|
2372
2287
|
}
|
|
2373
|
-
function resolveProjectSafe2(app, name, reply) {
|
|
2374
|
-
try {
|
|
2375
|
-
return resolveProject(app.db, name);
|
|
2376
|
-
} catch (e) {
|
|
2377
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2378
|
-
const err = e;
|
|
2379
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
2380
|
-
return null;
|
|
2381
|
-
}
|
|
2382
|
-
throw e;
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
2288
|
|
|
2386
2289
|
// ../api-routes/src/runs.ts
|
|
2387
2290
|
import crypto8 from "crypto";
|
|
@@ -2389,7 +2292,7 @@ import { eq as eq7, asc, desc } from "drizzle-orm";
|
|
|
2389
2292
|
|
|
2390
2293
|
// ../api-routes/src/run-queue.ts
|
|
2391
2294
|
import crypto7 from "crypto";
|
|
2392
|
-
import { and
|
|
2295
|
+
import { and, eq as eq6, or } from "drizzle-orm";
|
|
2393
2296
|
function queueRunIfProjectIdle(db, params) {
|
|
2394
2297
|
const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2395
2298
|
const kind = params.kind ?? "answer-visibility";
|
|
@@ -2397,7 +2300,7 @@ function queueRunIfProjectIdle(db, params) {
|
|
|
2397
2300
|
const runId = crypto7.randomUUID();
|
|
2398
2301
|
return db.transaction((tx) => {
|
|
2399
2302
|
const activeRun = tx.select().from(runs).where(
|
|
2400
|
-
|
|
2303
|
+
and(
|
|
2401
2304
|
eq6(runs.projectId, params.projectId),
|
|
2402
2305
|
or(eq6(runs.status, "queued"), eq6(runs.status, "running"))
|
|
2403
2306
|
)
|
|
@@ -2421,13 +2324,9 @@ function queueRunIfProjectIdle(db, params) {
|
|
|
2421
2324
|
// ../api-routes/src/runs.ts
|
|
2422
2325
|
async function runRoutes(app, opts) {
|
|
2423
2326
|
app.post("/projects/:name/runs", async (request, reply) => {
|
|
2424
|
-
const project =
|
|
2425
|
-
if (!project) return;
|
|
2327
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2426
2328
|
const kind = request.body?.kind ?? "answer-visibility";
|
|
2427
|
-
if (kind !== "answer-visibility")
|
|
2428
|
-
const err = unsupportedKind(kind);
|
|
2429
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2430
|
-
}
|
|
2329
|
+
if (kind !== "answer-visibility") throw unsupportedKind(kind);
|
|
2431
2330
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2432
2331
|
const trigger = request.body?.trigger ?? "manual";
|
|
2433
2332
|
const rawProviders = request.body?.providers;
|
|
@@ -2437,31 +2336,30 @@ async function runRoutes(app, opts) {
|
|
|
2437
2336
|
if (validNames.length) {
|
|
2438
2337
|
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
2439
2338
|
if (invalid.length) {
|
|
2440
|
-
|
|
2339
|
+
throw validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2441
2340
|
invalidProviders: invalid,
|
|
2442
2341
|
validProviders: validNames
|
|
2443
2342
|
});
|
|
2444
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2445
2343
|
}
|
|
2446
2344
|
}
|
|
2447
2345
|
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
2448
2346
|
}
|
|
2449
2347
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
2450
2348
|
let resolvedLocation;
|
|
2451
|
-
const projectLocations =
|
|
2349
|
+
const projectLocations = parseJsonColumn(project.locations, []);
|
|
2452
2350
|
if (request.body?.noLocation) {
|
|
2453
2351
|
resolvedLocation = null;
|
|
2454
2352
|
} else if (request.body?.allLocations) {
|
|
2455
2353
|
} else if (request.body?.location) {
|
|
2456
2354
|
const loc = projectLocations.find((l) => l.label === request.body.location);
|
|
2457
2355
|
if (!loc) {
|
|
2458
|
-
|
|
2356
|
+
throw validationError(`Location "${request.body.location}" not found. Configure it first.`);
|
|
2459
2357
|
}
|
|
2460
2358
|
resolvedLocation = loc;
|
|
2461
2359
|
}
|
|
2462
2360
|
if (request.body?.allLocations) {
|
|
2463
2361
|
if (projectLocations.length === 0) {
|
|
2464
|
-
|
|
2362
|
+
throw validationError("No locations configured for this project");
|
|
2465
2363
|
}
|
|
2466
2364
|
const newRuns = [];
|
|
2467
2365
|
for (const loc of projectLocations) {
|
|
@@ -2502,10 +2400,7 @@ async function runRoutes(app, opts) {
|
|
|
2502
2400
|
trigger,
|
|
2503
2401
|
location: locationLabel
|
|
2504
2402
|
});
|
|
2505
|
-
if (queueResult.conflict)
|
|
2506
|
-
const err = runInProgress(project.name);
|
|
2507
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2508
|
-
}
|
|
2403
|
+
if (queueResult.conflict) throw runInProgress(project.name);
|
|
2509
2404
|
const runId = queueResult.runId;
|
|
2510
2405
|
writeAuditLog(app.db, {
|
|
2511
2406
|
projectId: project.id,
|
|
@@ -2521,8 +2416,7 @@ async function runRoutes(app, opts) {
|
|
|
2521
2416
|
return reply.status(201).send(formatRun(run));
|
|
2522
2417
|
});
|
|
2523
2418
|
app.get("/projects/:name/runs", async (request, reply) => {
|
|
2524
|
-
const project =
|
|
2525
|
-
if (!project) return;
|
|
2419
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2526
2420
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
2527
2421
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
|
|
2528
2422
|
const rows = limit == null ? app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
|
|
@@ -2538,10 +2432,7 @@ async function runRoutes(app, opts) {
|
|
|
2538
2432
|
return reply.status(207).send([]);
|
|
2539
2433
|
}
|
|
2540
2434
|
const kind = request.body?.kind ?? "answer-visibility";
|
|
2541
|
-
if (kind !== "answer-visibility")
|
|
2542
|
-
const err = unsupportedKind(kind);
|
|
2543
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2544
|
-
}
|
|
2435
|
+
if (kind !== "answer-visibility") throw unsupportedKind(kind);
|
|
2545
2436
|
const rawProviders = request.body?.providers;
|
|
2546
2437
|
if (rawProviders?.length) {
|
|
2547
2438
|
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
@@ -2549,11 +2440,10 @@ async function runRoutes(app, opts) {
|
|
|
2549
2440
|
if (validNames.length) {
|
|
2550
2441
|
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
2551
2442
|
if (invalid.length) {
|
|
2552
|
-
|
|
2443
|
+
throw validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2553
2444
|
invalidProviders: invalid,
|
|
2554
2445
|
validProviders: validNames
|
|
2555
2446
|
});
|
|
2556
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2557
2447
|
}
|
|
2558
2448
|
}
|
|
2559
2449
|
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
@@ -2590,15 +2480,9 @@ async function runRoutes(app, opts) {
|
|
|
2590
2480
|
});
|
|
2591
2481
|
app.post("/runs/:id/cancel", async (request, reply) => {
|
|
2592
2482
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
2593
|
-
if (!run)
|
|
2594
|
-
const err = notFound("Run", request.params.id);
|
|
2595
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2596
|
-
}
|
|
2483
|
+
if (!run) throw notFound("Run", request.params.id);
|
|
2597
2484
|
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
2598
|
-
if (terminalStatuses.has(run.status))
|
|
2599
|
-
const err = runNotCancellable(run.id, run.status);
|
|
2600
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2601
|
-
}
|
|
2485
|
+
if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
|
|
2602
2486
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2603
2487
|
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: "Cancelled by user" }).where(eq7(runs.id, run.id)).run();
|
|
2604
2488
|
writeAuditLog(app.db, {
|
|
@@ -2613,9 +2497,7 @@ async function runRoutes(app, opts) {
|
|
|
2613
2497
|
});
|
|
2614
2498
|
app.get("/runs/:id", async (request, reply) => {
|
|
2615
2499
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
2616
|
-
if (!run)
|
|
2617
|
-
return reply.status(404).send({ error: { code: "NOT_FOUND", message: `Run '${request.params.id}' not found` } });
|
|
2618
|
-
}
|
|
2500
|
+
if (!run) throw notFound("Run", request.params.id);
|
|
2619
2501
|
const snapshots = app.db.select({
|
|
2620
2502
|
id: querySnapshots.id,
|
|
2621
2503
|
runId: querySnapshots.runId,
|
|
@@ -2644,9 +2526,9 @@ async function runRoutes(app, opts) {
|
|
|
2644
2526
|
provider: s.provider,
|
|
2645
2527
|
citationState: s.citationState,
|
|
2646
2528
|
answerText: s.answerText,
|
|
2647
|
-
citedDomains:
|
|
2648
|
-
competitorOverlap:
|
|
2649
|
-
recommendedCompetitors:
|
|
2529
|
+
citedDomains: parseJsonColumn(s.citedDomains, []),
|
|
2530
|
+
competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
|
|
2531
|
+
recommendedCompetitors: parseJsonColumn(s.recommendedCompetitors, []),
|
|
2650
2532
|
model: s.model ?? rawParsed.model,
|
|
2651
2533
|
location: s.location,
|
|
2652
2534
|
groundingSources: rawParsed.groundingSources,
|
|
@@ -2672,32 +2554,13 @@ function formatRun(row) {
|
|
|
2672
2554
|
};
|
|
2673
2555
|
}
|
|
2674
2556
|
function parseSnapshotRawResponse(raw) {
|
|
2675
|
-
const parsed =
|
|
2557
|
+
const parsed = parseJsonColumn(raw, {});
|
|
2676
2558
|
return {
|
|
2677
2559
|
groundingSources: parsed.groundingSources ?? [],
|
|
2678
2560
|
searchQueries: parsed.searchQueries ?? [],
|
|
2679
2561
|
model: parsed.model ?? null
|
|
2680
2562
|
};
|
|
2681
2563
|
}
|
|
2682
|
-
function tryParseJson(value, fallback) {
|
|
2683
|
-
try {
|
|
2684
|
-
return JSON.parse(value);
|
|
2685
|
-
} catch {
|
|
2686
|
-
return fallback;
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
function resolveProjectSafe3(app, name, reply) {
|
|
2690
|
-
try {
|
|
2691
|
-
return resolveProject(app.db, name);
|
|
2692
|
-
} catch (e) {
|
|
2693
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
2694
|
-
const err = e;
|
|
2695
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
2696
|
-
return null;
|
|
2697
|
-
}
|
|
2698
|
-
throw e;
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
2564
|
|
|
2702
2565
|
// ../api-routes/src/apply.ts
|
|
2703
2566
|
import crypto10 from "crypto";
|
|
@@ -2955,10 +2818,9 @@ async function applyRoutes(app, opts) {
|
|
|
2955
2818
|
app.post("/apply", async (request, reply) => {
|
|
2956
2819
|
const parsed = projectConfigSchema.safeParse(request.body);
|
|
2957
2820
|
if (!parsed.success) {
|
|
2958
|
-
|
|
2821
|
+
throw validationError("Invalid project config", {
|
|
2959
2822
|
issues: parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))
|
|
2960
2823
|
});
|
|
2961
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2962
2824
|
}
|
|
2963
2825
|
const config = parsed.data;
|
|
2964
2826
|
const validNames = opts?.validProviderNames ?? [];
|
|
@@ -2970,70 +2832,104 @@ async function applyRoutes(app, opts) {
|
|
|
2970
2832
|
if (allProviders.length) {
|
|
2971
2833
|
const invalid = allProviders.filter((p) => !validNames.includes(p));
|
|
2972
2834
|
if (invalid.length) {
|
|
2973
|
-
|
|
2835
|
+
throw validationError(`Invalid provider(s): ${[...new Set(invalid)].join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2974
2836
|
invalidProviders: [...new Set(invalid)],
|
|
2975
2837
|
validProviders: validNames
|
|
2976
2838
|
});
|
|
2977
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
2978
2839
|
}
|
|
2979
2840
|
}
|
|
2980
2841
|
}
|
|
2842
|
+
let resolvedSchedule = null;
|
|
2843
|
+
let deleteSchedule = false;
|
|
2844
|
+
if (config.spec.schedule) {
|
|
2845
|
+
const schedSpec = config.spec.schedule;
|
|
2846
|
+
let cronExpr;
|
|
2847
|
+
let preset = null;
|
|
2848
|
+
if (schedSpec.preset) {
|
|
2849
|
+
preset = schedSpec.preset;
|
|
2850
|
+
try {
|
|
2851
|
+
cronExpr = resolvePreset(schedSpec.preset);
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2854
|
+
throw validationError(msg);
|
|
2855
|
+
}
|
|
2856
|
+
} else if (schedSpec.cron) {
|
|
2857
|
+
cronExpr = schedSpec.cron;
|
|
2858
|
+
if (!validateCron(cronExpr)) throw validationError(`Invalid cron expression in schedule: ${cronExpr}`);
|
|
2859
|
+
} else {
|
|
2860
|
+
throw validationError('Schedule requires either "preset" or "cron"');
|
|
2861
|
+
}
|
|
2862
|
+
const timezone = schedSpec.timezone ?? "UTC";
|
|
2863
|
+
if (!isValidTimezone(timezone)) throw validationError(`Invalid timezone: ${timezone}`);
|
|
2864
|
+
resolvedSchedule = { cronExpr, preset, timezone };
|
|
2865
|
+
} else {
|
|
2866
|
+
deleteSchedule = true;
|
|
2867
|
+
}
|
|
2868
|
+
const rawSpec = request.body?.spec ?? {};
|
|
2869
|
+
const hasNotifications = "notifications" in rawSpec;
|
|
2870
|
+
if (hasNotifications) {
|
|
2871
|
+
for (const notif of config.spec.notifications) {
|
|
2872
|
+
const urlCheck = await resolveWebhookTarget(notif.url ?? "");
|
|
2873
|
+
if (!urlCheck.ok) throw validationError(`Notification URL invalid: ${urlCheck.message}`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2981
2876
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2982
2877
|
const name = config.metadata.name;
|
|
2983
|
-
const existing = app.db.select().from(projects).where(eq8(projects.name, name)).get();
|
|
2984
2878
|
let projectId;
|
|
2985
|
-
|
|
2986
|
-
projectId = existing.id;
|
|
2987
|
-
app.db.update(projects).set({
|
|
2988
|
-
displayName: config.spec.displayName,
|
|
2989
|
-
canonicalDomain: config.spec.canonicalDomain,
|
|
2990
|
-
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2991
|
-
country: config.spec.country,
|
|
2992
|
-
language: config.spec.language,
|
|
2993
|
-
labels: JSON.stringify(config.metadata.labels),
|
|
2994
|
-
providers: JSON.stringify(config.spec.providers ?? []),
|
|
2995
|
-
locations: JSON.stringify(config.spec.locations ?? []),
|
|
2996
|
-
defaultLocation: config.spec.defaultLocation ?? null,
|
|
2997
|
-
configSource: "config-file",
|
|
2998
|
-
configRevision: existing.configRevision + 1,
|
|
2999
|
-
updatedAt: now
|
|
3000
|
-
}).where(eq8(projects.id, existing.id)).run();
|
|
3001
|
-
writeAuditLog(app.db, {
|
|
3002
|
-
projectId,
|
|
3003
|
-
actor: "api",
|
|
3004
|
-
action: "project.applied",
|
|
3005
|
-
entityType: "project",
|
|
3006
|
-
entityId: projectId
|
|
3007
|
-
});
|
|
3008
|
-
} else {
|
|
3009
|
-
projectId = crypto10.randomUUID();
|
|
3010
|
-
app.db.insert(projects).values({
|
|
3011
|
-
id: projectId,
|
|
3012
|
-
name,
|
|
3013
|
-
displayName: config.spec.displayName,
|
|
3014
|
-
canonicalDomain: config.spec.canonicalDomain,
|
|
3015
|
-
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
3016
|
-
country: config.spec.country,
|
|
3017
|
-
language: config.spec.language,
|
|
3018
|
-
tags: "[]",
|
|
3019
|
-
labels: JSON.stringify(config.metadata.labels),
|
|
3020
|
-
providers: JSON.stringify(config.spec.providers ?? []),
|
|
3021
|
-
locations: JSON.stringify(config.spec.locations ?? []),
|
|
3022
|
-
defaultLocation: config.spec.defaultLocation ?? null,
|
|
3023
|
-
configSource: "config-file",
|
|
3024
|
-
configRevision: 1,
|
|
3025
|
-
createdAt: now,
|
|
3026
|
-
updatedAt: now
|
|
3027
|
-
}).run();
|
|
3028
|
-
writeAuditLog(app.db, {
|
|
3029
|
-
projectId,
|
|
3030
|
-
actor: "api",
|
|
3031
|
-
action: "project.created",
|
|
3032
|
-
entityType: "project",
|
|
3033
|
-
entityId: projectId
|
|
3034
|
-
});
|
|
3035
|
-
}
|
|
2879
|
+
let scheduleAction = null;
|
|
3036
2880
|
app.db.transaction((tx) => {
|
|
2881
|
+
const existing = tx.select().from(projects).where(eq8(projects.name, name)).get();
|
|
2882
|
+
if (existing) {
|
|
2883
|
+
projectId = existing.id;
|
|
2884
|
+
tx.update(projects).set({
|
|
2885
|
+
displayName: config.spec.displayName,
|
|
2886
|
+
canonicalDomain: config.spec.canonicalDomain,
|
|
2887
|
+
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2888
|
+
country: config.spec.country,
|
|
2889
|
+
language: config.spec.language,
|
|
2890
|
+
labels: JSON.stringify(config.metadata.labels),
|
|
2891
|
+
providers: JSON.stringify(config.spec.providers ?? []),
|
|
2892
|
+
locations: JSON.stringify(config.spec.locations ?? []),
|
|
2893
|
+
defaultLocation: config.spec.defaultLocation ?? null,
|
|
2894
|
+
configSource: "config-file",
|
|
2895
|
+
configRevision: existing.configRevision + 1,
|
|
2896
|
+
updatedAt: now
|
|
2897
|
+
}).where(eq8(projects.id, existing.id)).run();
|
|
2898
|
+
writeAuditLog(tx, {
|
|
2899
|
+
projectId,
|
|
2900
|
+
actor: "api",
|
|
2901
|
+
action: "project.applied",
|
|
2902
|
+
entityType: "project",
|
|
2903
|
+
entityId: projectId
|
|
2904
|
+
});
|
|
2905
|
+
} else {
|
|
2906
|
+
projectId = crypto10.randomUUID();
|
|
2907
|
+
tx.insert(projects).values({
|
|
2908
|
+
id: projectId,
|
|
2909
|
+
name,
|
|
2910
|
+
displayName: config.spec.displayName,
|
|
2911
|
+
canonicalDomain: config.spec.canonicalDomain,
|
|
2912
|
+
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2913
|
+
country: config.spec.country,
|
|
2914
|
+
language: config.spec.language,
|
|
2915
|
+
tags: "[]",
|
|
2916
|
+
labels: JSON.stringify(config.metadata.labels),
|
|
2917
|
+
providers: JSON.stringify(config.spec.providers ?? []),
|
|
2918
|
+
locations: JSON.stringify(config.spec.locations ?? []),
|
|
2919
|
+
defaultLocation: config.spec.defaultLocation ?? null,
|
|
2920
|
+
configSource: "config-file",
|
|
2921
|
+
configRevision: 1,
|
|
2922
|
+
createdAt: now,
|
|
2923
|
+
updatedAt: now
|
|
2924
|
+
}).run();
|
|
2925
|
+
writeAuditLog(tx, {
|
|
2926
|
+
projectId,
|
|
2927
|
+
actor: "api",
|
|
2928
|
+
action: "project.created",
|
|
2929
|
+
entityType: "project",
|
|
2930
|
+
entityId: projectId
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
3037
2933
|
tx.delete(keywords).where(eq8(keywords.projectId, projectId)).run();
|
|
3038
2934
|
for (const kw of config.spec.keywords) {
|
|
3039
2935
|
tx.insert(keywords).values({
|
|
@@ -3066,98 +2962,63 @@ async function applyRoutes(app, opts) {
|
|
|
3066
2962
|
entityType: "competitor",
|
|
3067
2963
|
diff: { competitors: config.spec.competitors }
|
|
3068
2964
|
});
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
2965
|
+
if (resolvedSchedule) {
|
|
2966
|
+
const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
|
|
2967
|
+
if (existingSched) {
|
|
2968
|
+
tx.update(schedules).set({
|
|
2969
|
+
cronExpr: resolvedSchedule.cronExpr,
|
|
2970
|
+
preset: resolvedSchedule.preset,
|
|
2971
|
+
timezone: resolvedSchedule.timezone,
|
|
2972
|
+
providers: JSON.stringify(config.spec.schedule?.providers ?? []),
|
|
2973
|
+
enabled: 1,
|
|
2974
|
+
updatedAt: now
|
|
2975
|
+
}).where(eq8(schedules.id, existingSched.id)).run();
|
|
2976
|
+
} else {
|
|
2977
|
+
tx.insert(schedules).values({
|
|
2978
|
+
id: crypto10.randomUUID(),
|
|
2979
|
+
projectId,
|
|
2980
|
+
cronExpr: resolvedSchedule.cronExpr,
|
|
2981
|
+
preset: resolvedSchedule.preset,
|
|
2982
|
+
timezone: resolvedSchedule.timezone,
|
|
2983
|
+
enabled: 1,
|
|
2984
|
+
providers: JSON.stringify(config.spec.schedule?.providers ?? []),
|
|
2985
|
+
createdAt: now,
|
|
2986
|
+
updatedAt: now
|
|
2987
|
+
}).run();
|
|
3081
2988
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
2989
|
+
scheduleAction = "upsert";
|
|
2990
|
+
} else if (deleteSchedule) {
|
|
2991
|
+
const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
|
|
2992
|
+
if (existingSched) {
|
|
2993
|
+
tx.delete(schedules).where(eq8(schedules.projectId, projectId)).run();
|
|
2994
|
+
scheduleAction = "delete";
|
|
3088
2995
|
}
|
|
3089
|
-
} else {
|
|
3090
|
-
return reply.status(400).send({
|
|
3091
|
-
error: { code: "VALIDATION_ERROR", message: 'Schedule requires either "preset" or "cron"' }
|
|
3092
|
-
});
|
|
3093
|
-
}
|
|
3094
|
-
const timezone = schedSpec.timezone ?? "UTC";
|
|
3095
|
-
if (!isValidTimezone(timezone)) {
|
|
3096
|
-
return reply.status(400).send({
|
|
3097
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid timezone: ${timezone}` }
|
|
3098
|
-
});
|
|
3099
2996
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
projectId,
|
|
3114
|
-
cronExpr,
|
|
3115
|
-
preset,
|
|
3116
|
-
timezone,
|
|
3117
|
-
enabled: 1,
|
|
3118
|
-
providers: JSON.stringify(schedSpec.providers ?? []),
|
|
3119
|
-
createdAt: now,
|
|
3120
|
-
updatedAt: now
|
|
3121
|
-
}).run();
|
|
3122
|
-
}
|
|
3123
|
-
opts?.onScheduleUpdated?.("upsert", projectId);
|
|
3124
|
-
} else {
|
|
3125
|
-
const existingSched = app.db.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
|
|
3126
|
-
if (existingSched) {
|
|
3127
|
-
app.db.delete(schedules).where(eq8(schedules.projectId, projectId)).run();
|
|
3128
|
-
opts?.onScheduleUpdated?.("delete", projectId);
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
const rawSpec = request.body?.spec ?? {};
|
|
3132
|
-
if ("notifications" in rawSpec) {
|
|
3133
|
-
for (const notif of config.spec.notifications) {
|
|
3134
|
-
const urlCheck = await resolveWebhookTarget(notif.url ?? "");
|
|
3135
|
-
if (!urlCheck.ok) {
|
|
3136
|
-
return reply.status(400).send({
|
|
3137
|
-
error: { code: "VALIDATION_ERROR", message: `Notification URL invalid: ${urlCheck.message}` }
|
|
3138
|
-
});
|
|
2997
|
+
if (hasNotifications) {
|
|
2998
|
+
tx.delete(notifications).where(eq8(notifications.projectId, projectId)).run();
|
|
2999
|
+
for (const notif of config.spec.notifications) {
|
|
3000
|
+
tx.insert(notifications).values({
|
|
3001
|
+
id: crypto10.randomUUID(),
|
|
3002
|
+
projectId,
|
|
3003
|
+
channel: notif.channel,
|
|
3004
|
+
config: JSON.stringify({ url: notif.url, events: notif.events }),
|
|
3005
|
+
webhookSecret: crypto10.randomBytes(32).toString("hex"),
|
|
3006
|
+
enabled: 1,
|
|
3007
|
+
createdAt: now,
|
|
3008
|
+
updatedAt: now
|
|
3009
|
+
}).run();
|
|
3139
3010
|
}
|
|
3140
|
-
|
|
3141
|
-
app.db.delete(notifications).where(eq8(notifications.projectId, projectId)).run();
|
|
3142
|
-
for (const notif of config.spec.notifications) {
|
|
3143
|
-
app.db.insert(notifications).values({
|
|
3144
|
-
id: crypto10.randomUUID(),
|
|
3011
|
+
writeAuditLog(tx, {
|
|
3145
3012
|
projectId,
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
updatedAt: now
|
|
3152
|
-
}).run();
|
|
3013
|
+
actor: "api",
|
|
3014
|
+
action: "notifications.replaced",
|
|
3015
|
+
entityType: "notification",
|
|
3016
|
+
diff: { notifications: config.spec.notifications }
|
|
3017
|
+
});
|
|
3153
3018
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
action: "notifications.replaced",
|
|
3158
|
-
entityType: "notification",
|
|
3159
|
-
diff: { notifications: config.spec.notifications }
|
|
3160
|
-
});
|
|
3019
|
+
});
|
|
3020
|
+
if (scheduleAction) {
|
|
3021
|
+
opts?.onScheduleUpdated?.(scheduleAction, projectId);
|
|
3161
3022
|
}
|
|
3162
3023
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
3163
3024
|
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
@@ -3168,13 +3029,13 @@ async function applyRoutes(app, opts) {
|
|
|
3168
3029
|
name: project.name,
|
|
3169
3030
|
displayName: project.displayName,
|
|
3170
3031
|
canonicalDomain: project.canonicalDomain,
|
|
3171
|
-
ownedDomains:
|
|
3032
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, []),
|
|
3172
3033
|
country: project.country,
|
|
3173
3034
|
language: project.language,
|
|
3174
|
-
tags:
|
|
3175
|
-
labels:
|
|
3176
|
-
providers:
|
|
3177
|
-
locations:
|
|
3035
|
+
tags: parseJsonColumn(project.tags, []),
|
|
3036
|
+
labels: parseJsonColumn(project.labels, {}),
|
|
3037
|
+
providers: parseJsonColumn(project.providers, []),
|
|
3038
|
+
locations: parseJsonColumn(project.locations, []),
|
|
3178
3039
|
defaultLocation: project.defaultLocation,
|
|
3179
3040
|
configSource: project.configSource,
|
|
3180
3041
|
configRevision: project.configRevision,
|
|
@@ -3230,8 +3091,7 @@ function redactNotificationDiff(value) {
|
|
|
3230
3091
|
// ../api-routes/src/history.ts
|
|
3231
3092
|
async function historyRoutes(app) {
|
|
3232
3093
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
3233
|
-
const project =
|
|
3234
|
-
if (!project) return;
|
|
3094
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3235
3095
|
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
3236
3096
|
return reply.send(rows.map(formatAuditEntry));
|
|
3237
3097
|
});
|
|
@@ -3240,8 +3100,7 @@ async function historyRoutes(app) {
|
|
|
3240
3100
|
return reply.send(rows.map(formatAuditEntry));
|
|
3241
3101
|
});
|
|
3242
3102
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
3243
|
-
const project =
|
|
3244
|
-
if (!project) return;
|
|
3103
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3245
3104
|
const limit = parseInt(request.query.limit ?? "50", 10);
|
|
3246
3105
|
const offset = parseInt(request.query.offset ?? "0", 10);
|
|
3247
3106
|
const projectRuns = app.db.select({ id: runs.id }).from(runs).where(eq9(runs.projectId, project.id)).all();
|
|
@@ -3277,9 +3136,9 @@ async function historyRoutes(app) {
|
|
|
3277
3136
|
model: s.model,
|
|
3278
3137
|
citationState: s.citationState,
|
|
3279
3138
|
answerText: s.answerText,
|
|
3280
|
-
citedDomains:
|
|
3281
|
-
competitorOverlap:
|
|
3282
|
-
recommendedCompetitors:
|
|
3139
|
+
citedDomains: parseJsonColumn(s.citedDomains, []),
|
|
3140
|
+
competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
|
|
3141
|
+
recommendedCompetitors: parseJsonColumn(s.recommendedCompetitors, []),
|
|
3283
3142
|
location: s.location,
|
|
3284
3143
|
createdAt: s.createdAt
|
|
3285
3144
|
})),
|
|
@@ -3287,8 +3146,7 @@ async function historyRoutes(app) {
|
|
|
3287
3146
|
});
|
|
3288
3147
|
});
|
|
3289
3148
|
app.get("/projects/:name/timeline", async (request, reply) => {
|
|
3290
|
-
const project =
|
|
3291
|
-
if (!project) return;
|
|
3149
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3292
3150
|
const projectKeywords = app.db.select().from(keywords).where(eq9(keywords.projectId, project.id)).all();
|
|
3293
3151
|
const projectRuns = app.db.select().from(runs).where(eq9(runs.projectId, project.id)).orderBy(runs.createdAt).all();
|
|
3294
3152
|
if (projectRuns.length === 0 || projectKeywords.length === 0) {
|
|
@@ -3370,11 +3228,10 @@ async function historyRoutes(app) {
|
|
|
3370
3228
|
return reply.send(timeline);
|
|
3371
3229
|
});
|
|
3372
3230
|
app.get("/projects/:name/snapshots/diff", async (request, reply) => {
|
|
3373
|
-
|
|
3374
|
-
if (!project) return;
|
|
3231
|
+
resolveProject(app.db, request.params.name);
|
|
3375
3232
|
const { run1, run2 } = request.query;
|
|
3376
3233
|
if (!run1 || !run2) {
|
|
3377
|
-
|
|
3234
|
+
throw validationError("Both run1 and run2 query params are required");
|
|
3378
3235
|
}
|
|
3379
3236
|
const snaps1 = app.db.select({
|
|
3380
3237
|
keywordId: querySnapshots.keywordId,
|
|
@@ -3419,36 +3276,16 @@ function formatAuditEntry(row) {
|
|
|
3419
3276
|
action: row.action,
|
|
3420
3277
|
entityType: row.entityType,
|
|
3421
3278
|
entityId: row.entityId,
|
|
3422
|
-
diff: row.diff ? row.entityType === "notification" ? redactNotificationDiff(
|
|
3279
|
+
diff: row.diff ? row.entityType === "notification" ? redactNotificationDiff(parseJsonColumn(row.diff, null)) : parseJsonColumn(row.diff, null) : null,
|
|
3423
3280
|
createdAt: row.createdAt
|
|
3424
3281
|
};
|
|
3425
3282
|
}
|
|
3426
|
-
function tryParseJson2(value, fallback) {
|
|
3427
|
-
try {
|
|
3428
|
-
return JSON.parse(value);
|
|
3429
|
-
} catch {
|
|
3430
|
-
return fallback;
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
function resolveProjectSafe4(app, name, reply) {
|
|
3434
|
-
try {
|
|
3435
|
-
return resolveProject(app.db, name);
|
|
3436
|
-
} catch (e) {
|
|
3437
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
3438
|
-
const err = e;
|
|
3439
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
3440
|
-
return null;
|
|
3441
|
-
}
|
|
3442
|
-
throw e;
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
3283
|
|
|
3446
3284
|
// ../api-routes/src/analytics.ts
|
|
3447
3285
|
import { eq as eq10, desc as desc3, inArray as inArray2 } from "drizzle-orm";
|
|
3448
3286
|
async function analyticsRoutes(app) {
|
|
3449
3287
|
app.get("/projects/:name/analytics/metrics", async (request, reply) => {
|
|
3450
|
-
const project =
|
|
3451
|
-
if (!project) return;
|
|
3288
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3452
3289
|
const window = parseWindow(request.query.window);
|
|
3453
3290
|
const cutoff = windowCutoff(window);
|
|
3454
3291
|
const projectRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
@@ -3488,8 +3325,7 @@ async function analyticsRoutes(app) {
|
|
|
3488
3325
|
return reply.send({ window, buckets, overall, byProvider, trend, keywordChanges });
|
|
3489
3326
|
});
|
|
3490
3327
|
app.get("/projects/:name/analytics/gaps", async (request, reply) => {
|
|
3491
|
-
const project =
|
|
3492
|
-
if (!project) return;
|
|
3328
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3493
3329
|
const window = parseWindow(request.query.window);
|
|
3494
3330
|
const cutoff = windowCutoff(window);
|
|
3495
3331
|
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
|
|
@@ -3537,7 +3373,7 @@ async function analyticsRoutes(app) {
|
|
|
3537
3373
|
const citedProviders = kwSnapshots.filter((s) => s.citationState === "cited").map((s) => s.provider);
|
|
3538
3374
|
const competitorsCiting = /* @__PURE__ */ new Set();
|
|
3539
3375
|
for (const s of kwSnapshots) {
|
|
3540
|
-
const overlap =
|
|
3376
|
+
const overlap = parseJsonColumn(s.competitorOverlap, []);
|
|
3541
3377
|
for (const c of overlap) competitorsCiting.add(c);
|
|
3542
3378
|
}
|
|
3543
3379
|
let category;
|
|
@@ -3570,8 +3406,7 @@ async function analyticsRoutes(app) {
|
|
|
3570
3406
|
return reply.send({ cited, gap, uncited, runId: latestRun.id, window });
|
|
3571
3407
|
});
|
|
3572
3408
|
app.get("/projects/:name/analytics/sources", async (request, reply) => {
|
|
3573
|
-
const project =
|
|
3574
|
-
if (!project) return;
|
|
3409
|
+
const project = resolveProject(app.db, request.params.name);
|
|
3575
3410
|
const window = parseWindow(request.query.window);
|
|
3576
3411
|
const cutoff = windowCutoff(window);
|
|
3577
3412
|
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
@@ -3607,26 +3442,6 @@ async function analyticsRoutes(app) {
|
|
|
3607
3442
|
return reply.send({ overall, byKeyword, runId: latestRunId, window });
|
|
3608
3443
|
});
|
|
3609
3444
|
}
|
|
3610
|
-
function resolveProjectSafe5(app, name, reply) {
|
|
3611
|
-
try {
|
|
3612
|
-
return resolveProject(app.db, name);
|
|
3613
|
-
} catch (e) {
|
|
3614
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
3615
|
-
const err = e;
|
|
3616
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
3617
|
-
return null;
|
|
3618
|
-
}
|
|
3619
|
-
throw e;
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
|
-
function tryParseJson3(value, fallback) {
|
|
3623
|
-
if (!value) return fallback;
|
|
3624
|
-
try {
|
|
3625
|
-
return JSON.parse(value);
|
|
3626
|
-
} catch {
|
|
3627
|
-
return fallback;
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
3445
|
var PROVIDER_INFRA_DOMAINS = /* @__PURE__ */ new Set([
|
|
3631
3446
|
"vertexaisearch.cloud.google.com",
|
|
3632
3447
|
"openai.com",
|
|
@@ -3644,7 +3459,7 @@ function isProviderInfraDomain(uri) {
|
|
|
3644
3459
|
return false;
|
|
3645
3460
|
}
|
|
3646
3461
|
function parseGroundingSources(rawResponse) {
|
|
3647
|
-
const parsed =
|
|
3462
|
+
const parsed = parseJsonColumn(rawResponse, {});
|
|
3648
3463
|
const sources = parsed.groundingSources;
|
|
3649
3464
|
if (!Array.isArray(sources)) return [];
|
|
3650
3465
|
return sources.filter(
|
|
@@ -6014,34 +5829,29 @@ import crypto11 from "crypto";
|
|
|
6014
5829
|
import { eq as eq11 } from "drizzle-orm";
|
|
6015
5830
|
async function scheduleRoutes(app, opts) {
|
|
6016
5831
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
6017
|
-
const project =
|
|
6018
|
-
if (!project) return;
|
|
5832
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6019
5833
|
const parsedBody = scheduleUpsertRequestSchema.safeParse(request.body);
|
|
6020
5834
|
if (!parsedBody.success) {
|
|
6021
|
-
|
|
5835
|
+
throw validationError("Invalid schedule payload", {
|
|
6022
5836
|
issues: parsedBody.error.issues.map((issue) => ({
|
|
6023
5837
|
path: issue.path.join("."),
|
|
6024
5838
|
message: issue.message
|
|
6025
5839
|
}))
|
|
6026
5840
|
});
|
|
6027
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
6028
5841
|
}
|
|
6029
5842
|
const { preset, cron: cron2, timezone, providers, enabled } = parsedBody.data;
|
|
6030
5843
|
const validNames = opts.validProviderNames ?? [];
|
|
6031
5844
|
if (validNames.length && providers?.length) {
|
|
6032
5845
|
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
6033
5846
|
if (invalid.length) {
|
|
6034
|
-
|
|
5847
|
+
throw validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
6035
5848
|
invalidProviders: invalid,
|
|
6036
5849
|
validProviders: validNames
|
|
6037
5850
|
});
|
|
6038
|
-
return reply.status(err.statusCode).send(err.toJSON());
|
|
6039
5851
|
}
|
|
6040
5852
|
}
|
|
6041
5853
|
if (!isValidTimezone(timezone)) {
|
|
6042
|
-
|
|
6043
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid timezone: ${timezone}` }
|
|
6044
|
-
});
|
|
5854
|
+
throw validationError(`Invalid timezone: ${timezone}`);
|
|
6045
5855
|
}
|
|
6046
5856
|
let cronExpr;
|
|
6047
5857
|
if (preset) {
|
|
@@ -6049,14 +5859,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
6049
5859
|
cronExpr = resolvePreset(preset);
|
|
6050
5860
|
} catch (err) {
|
|
6051
5861
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6052
|
-
|
|
5862
|
+
throw validationError(msg);
|
|
6053
5863
|
}
|
|
6054
5864
|
} else {
|
|
6055
5865
|
cronExpr = cron2;
|
|
6056
5866
|
if (!validateCron(cronExpr)) {
|
|
6057
|
-
|
|
6058
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid cron expression: ${cronExpr}` }
|
|
6059
|
-
});
|
|
5867
|
+
throw validationError(`Invalid cron expression: ${cronExpr}`);
|
|
6060
5868
|
}
|
|
6061
5869
|
}
|
|
6062
5870
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -6096,20 +5904,18 @@ async function scheduleRoutes(app, opts) {
|
|
|
6096
5904
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
6097
5905
|
});
|
|
6098
5906
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
6099
|
-
const project =
|
|
6100
|
-
if (!project) return;
|
|
5907
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6101
5908
|
const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
|
|
6102
5909
|
if (!schedule) {
|
|
6103
|
-
|
|
5910
|
+
throw notFound("Schedule", request.params.name);
|
|
6104
5911
|
}
|
|
6105
5912
|
return reply.send(formatSchedule(schedule));
|
|
6106
5913
|
});
|
|
6107
5914
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
6108
|
-
const project =
|
|
6109
|
-
if (!project) return;
|
|
5915
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6110
5916
|
const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
|
|
6111
5917
|
if (!schedule) {
|
|
6112
|
-
|
|
5918
|
+
throw notFound("Schedule", request.params.name);
|
|
6113
5919
|
}
|
|
6114
5920
|
app.db.delete(schedules).where(eq11(schedules.id, schedule.id)).run();
|
|
6115
5921
|
writeAuditLog(app.db, {
|
|
@@ -6131,25 +5937,13 @@ function formatSchedule(row) {
|
|
|
6131
5937
|
preset: row.preset,
|
|
6132
5938
|
timezone: row.timezone,
|
|
6133
5939
|
enabled: row.enabled === 1,
|
|
6134
|
-
providers:
|
|
5940
|
+
providers: parseJsonColumn(row.providers, []),
|
|
6135
5941
|
lastRunAt: row.lastRunAt,
|
|
6136
5942
|
nextRunAt: row.nextRunAt,
|
|
6137
5943
|
createdAt: row.createdAt,
|
|
6138
5944
|
updatedAt: row.updatedAt
|
|
6139
5945
|
};
|
|
6140
5946
|
}
|
|
6141
|
-
function resolveProjectSafe6(app, name, reply) {
|
|
6142
|
-
try {
|
|
6143
|
-
return resolveProject(app.db, name);
|
|
6144
|
-
} catch (e) {
|
|
6145
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
6146
|
-
const err = e;
|
|
6147
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
6148
|
-
return null;
|
|
6149
|
-
}
|
|
6150
|
-
throw e;
|
|
6151
|
-
}
|
|
6152
|
-
}
|
|
6153
5947
|
|
|
6154
5948
|
// ../api-routes/src/notifications.ts
|
|
6155
5949
|
import crypto12 from "crypto";
|
|
@@ -6160,30 +5954,15 @@ async function notificationRoutes(app) {
|
|
|
6160
5954
|
return reply.send(VALID_EVENTS);
|
|
6161
5955
|
});
|
|
6162
5956
|
app.post("/projects/:name/notifications", async (request, reply) => {
|
|
6163
|
-
const project =
|
|
6164
|
-
if (!project) return;
|
|
5957
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6165
5958
|
const { channel, url, events } = request.body ?? {};
|
|
6166
|
-
if (channel !== "webhook")
|
|
6167
|
-
return reply.status(400).send({
|
|
6168
|
-
error: { code: "VALIDATION_ERROR", message: 'Only "webhook" channel is supported' }
|
|
6169
|
-
});
|
|
6170
|
-
}
|
|
5959
|
+
if (channel !== "webhook") throw validationError('Only "webhook" channel is supported');
|
|
6171
5960
|
const urlCheck = await resolveWebhookTarget(url ?? "");
|
|
6172
|
-
if (!urlCheck.ok)
|
|
6173
|
-
|
|
6174
|
-
error: { code: "VALIDATION_ERROR", message: urlCheck.message }
|
|
6175
|
-
});
|
|
6176
|
-
}
|
|
6177
|
-
if (!events?.length) {
|
|
6178
|
-
return reply.status(400).send({
|
|
6179
|
-
error: { code: "VALIDATION_ERROR", message: '"events" must be a non-empty array' }
|
|
6180
|
-
});
|
|
6181
|
-
}
|
|
5961
|
+
if (!urlCheck.ok) throw validationError(urlCheck.message);
|
|
5962
|
+
if (!events?.length) throw validationError('"events" must be a non-empty array');
|
|
6182
5963
|
const invalid = events.filter((e) => !VALID_EVENTS.includes(e));
|
|
6183
5964
|
if (invalid.length) {
|
|
6184
|
-
|
|
6185
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid event(s): ${invalid.join(", ")}. Must be one of: ${VALID_EVENTS.join(", ")}` }
|
|
6186
|
-
});
|
|
5965
|
+
throw validationError(`Invalid event(s): ${invalid.join(", ")}. Must be one of: ${VALID_EVENTS.join(", ")}`);
|
|
6187
5966
|
}
|
|
6188
5967
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6189
5968
|
const id = crypto12.randomUUID();
|
|
@@ -6212,19 +5991,15 @@ async function notificationRoutes(app) {
|
|
|
6212
5991
|
});
|
|
6213
5992
|
});
|
|
6214
5993
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
6215
|
-
const project =
|
|
6216
|
-
if (!project) return;
|
|
5994
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6217
5995
|
const rows = app.db.select().from(notifications).where(eq12(notifications.projectId, project.id)).all();
|
|
6218
5996
|
return reply.send(rows.map(formatNotification));
|
|
6219
5997
|
});
|
|
6220
5998
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
6221
|
-
const project =
|
|
6222
|
-
if (!project) return;
|
|
5999
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6223
6000
|
const notification = app.db.select().from(notifications).where(eq12(notifications.id, request.params.id)).get();
|
|
6224
6001
|
if (!notification || notification.projectId !== project.id) {
|
|
6225
|
-
|
|
6226
|
-
error: { code: "NOT_FOUND", message: `Notification '${request.params.id}' not found` }
|
|
6227
|
-
});
|
|
6002
|
+
throw notFound("Notification", request.params.id);
|
|
6228
6003
|
}
|
|
6229
6004
|
app.db.delete(notifications).where(eq12(notifications.id, notification.id)).run();
|
|
6230
6005
|
writeAuditLog(app.db, {
|
|
@@ -6237,21 +6012,14 @@ async function notificationRoutes(app) {
|
|
|
6237
6012
|
return reply.status(204).send();
|
|
6238
6013
|
});
|
|
6239
6014
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
6240
|
-
const project =
|
|
6241
|
-
if (!project) return;
|
|
6015
|
+
const project = resolveProject(app.db, request.params.name);
|
|
6242
6016
|
const notification = app.db.select().from(notifications).where(eq12(notifications.id, request.params.id)).get();
|
|
6243
6017
|
if (!notification || notification.projectId !== project.id) {
|
|
6244
|
-
|
|
6245
|
-
error: { code: "NOT_FOUND", message: `Notification '${request.params.id}' not found` }
|
|
6246
|
-
});
|
|
6018
|
+
throw notFound("Notification", request.params.id);
|
|
6247
6019
|
}
|
|
6248
|
-
const config =
|
|
6020
|
+
const config = parseJsonColumn(notification.config, { url: "", events: [] });
|
|
6249
6021
|
const urlCheck = await resolveWebhookTarget(config.url);
|
|
6250
|
-
if (!urlCheck.ok) {
|
|
6251
|
-
return reply.status(400).send({
|
|
6252
|
-
error: { code: "VALIDATION_ERROR", message: `Stored webhook URL is invalid: ${urlCheck.message}` }
|
|
6253
|
-
});
|
|
6254
|
-
}
|
|
6022
|
+
if (!urlCheck.ok) throw validationError(`Stored webhook URL is invalid: ${urlCheck.message}`);
|
|
6255
6023
|
const payload = {
|
|
6256
6024
|
source: "canonry",
|
|
6257
6025
|
event: "run.completed",
|
|
@@ -6274,14 +6042,12 @@ async function notificationRoutes(app) {
|
|
|
6274
6042
|
entityId: notification.id,
|
|
6275
6043
|
diff: { status, error }
|
|
6276
6044
|
});
|
|
6277
|
-
if (error)
|
|
6278
|
-
return reply.status(502).send({ error: { code: "DELIVERY_FAILED", message: error } });
|
|
6279
|
-
}
|
|
6045
|
+
if (error) throw deliveryFailed(error);
|
|
6280
6046
|
return reply.send({ status, ok: status >= 200 && status < 300 });
|
|
6281
6047
|
});
|
|
6282
6048
|
}
|
|
6283
6049
|
function formatNotification(row) {
|
|
6284
|
-
const config =
|
|
6050
|
+
const config = parseJsonColumn(row.config, { url: "", events: [] });
|
|
6285
6051
|
const redacted = redactNotificationUrl(config.url);
|
|
6286
6052
|
return {
|
|
6287
6053
|
id: row.id,
|
|
@@ -6296,22 +6062,10 @@ function formatNotification(row) {
|
|
|
6296
6062
|
updatedAt: row.updatedAt
|
|
6297
6063
|
};
|
|
6298
6064
|
}
|
|
6299
|
-
function resolveProjectSafe7(app, name, reply) {
|
|
6300
|
-
try {
|
|
6301
|
-
return resolveProject(app.db, name);
|
|
6302
|
-
} catch (e) {
|
|
6303
|
-
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
6304
|
-
const err = e;
|
|
6305
|
-
reply.status(err.statusCode).send(err.toJSON());
|
|
6306
|
-
return null;
|
|
6307
|
-
}
|
|
6308
|
-
throw e;
|
|
6309
|
-
}
|
|
6310
|
-
}
|
|
6311
6065
|
|
|
6312
6066
|
// ../api-routes/src/google.ts
|
|
6313
6067
|
import crypto13 from "crypto";
|
|
6314
|
-
import { eq as eq13, and as
|
|
6068
|
+
import { eq as eq13, and as and2, desc as desc4, sql as sql3 } from "drizzle-orm";
|
|
6315
6069
|
|
|
6316
6070
|
// ../integration-google/src/constants.ts
|
|
6317
6071
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -6758,11 +6512,11 @@ async function googleRoutes(app, opts) {
|
|
|
6758
6512
|
const project = resolveProject(app.db, request.params.name);
|
|
6759
6513
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
6760
6514
|
const conditions = [eq13(gscSearchData.projectId, project.id)];
|
|
6761
|
-
if (startDate) conditions.push(
|
|
6762
|
-
if (endDate) conditions.push(
|
|
6763
|
-
if (query) conditions.push(
|
|
6764
|
-
if (page) conditions.push(
|
|
6765
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
6515
|
+
if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
|
|
6516
|
+
if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
|
|
6517
|
+
if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
6518
|
+
if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
6519
|
+
const rows = app.db.select().from(gscSearchData).where(and2(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
6766
6520
|
return rows.map((r) => ({
|
|
6767
6521
|
date: r.date,
|
|
6768
6522
|
query: r.query,
|
|
@@ -6840,7 +6594,7 @@ async function googleRoutes(app, opts) {
|
|
|
6840
6594
|
const { url, limit } = request.query;
|
|
6841
6595
|
const conditions = [eq13(gscUrlInspections.projectId, project.id)];
|
|
6842
6596
|
if (url) conditions.push(eq13(gscUrlInspections.url, url));
|
|
6843
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
6597
|
+
const rows = app.db.select().from(gscUrlInspections).where(and2(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
6844
6598
|
return rows.map((r) => ({
|
|
6845
6599
|
id: r.id,
|
|
6846
6600
|
url: r.url,
|
|
@@ -7212,7 +6966,7 @@ async function googleRoutes(app, opts) {
|
|
|
7212
6966
|
|
|
7213
6967
|
// ../api-routes/src/bing.ts
|
|
7214
6968
|
import crypto14 from "crypto";
|
|
7215
|
-
import { eq as eq14, and as
|
|
6969
|
+
import { eq as eq14, and as and3, desc as desc5 } from "drizzle-orm";
|
|
7216
6970
|
|
|
7217
6971
|
// ../integration-bing/src/constants.ts
|
|
7218
6972
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -7511,7 +7265,7 @@ async function bingRoutes(app, opts) {
|
|
|
7511
7265
|
if (!store) return;
|
|
7512
7266
|
const project = resolveProject(app.db, request.params.name);
|
|
7513
7267
|
const { url, limit } = request.query;
|
|
7514
|
-
const whereClause = url ?
|
|
7268
|
+
const whereClause = url ? and3(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
|
|
7515
7269
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc5(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
7516
7270
|
return filtered.map((r) => ({
|
|
7517
7271
|
id: r.id,
|
|
@@ -7701,7 +7455,7 @@ async function bingRoutes(app, opts) {
|
|
|
7701
7455
|
import fs2 from "fs";
|
|
7702
7456
|
import path2 from "path";
|
|
7703
7457
|
import os2 from "os";
|
|
7704
|
-
import { eq as eq15, and as
|
|
7458
|
+
import { eq as eq15, and as and4 } from "drizzle-orm";
|
|
7705
7459
|
function getScreenshotDir() {
|
|
7706
7460
|
return path2.join(os2.homedir(), ".canonry", "screenshots");
|
|
7707
7461
|
}
|
|
@@ -7774,7 +7528,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7774
7528
|
async (request, reply) => {
|
|
7775
7529
|
const project = resolveProject(app.db, request.params.name);
|
|
7776
7530
|
const { runId } = request.params;
|
|
7777
|
-
const run = app.db.select().from(runs).where(
|
|
7531
|
+
const run = app.db.select().from(runs).where(and4(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
|
|
7778
7532
|
if (!run) {
|
|
7779
7533
|
const err = notFound("Run", runId);
|
|
7780
7534
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7871,7 +7625,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7871
7625
|
|
|
7872
7626
|
// ../api-routes/src/ga.ts
|
|
7873
7627
|
import crypto16 from "crypto";
|
|
7874
|
-
import { eq as eq16, desc as desc6, and as
|
|
7628
|
+
import { eq as eq16, desc as desc6, and as and5, sql as sql4 } from "drizzle-orm";
|
|
7875
7629
|
|
|
7876
7630
|
// ../integration-google-analytics/src/ga4-client.ts
|
|
7877
7631
|
import crypto15 from "crypto";
|
|
@@ -8351,10 +8105,10 @@ async function ga4Routes(app, opts) {
|
|
|
8351
8105
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8352
8106
|
app.db.transaction((tx) => {
|
|
8353
8107
|
tx.delete(gaTrafficSnapshots).where(
|
|
8354
|
-
|
|
8108
|
+
and5(
|
|
8355
8109
|
eq16(gaTrafficSnapshots.projectId, project.id),
|
|
8356
|
-
|
|
8357
|
-
|
|
8110
|
+
sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
8111
|
+
sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
8358
8112
|
)
|
|
8359
8113
|
).run();
|
|
8360
8114
|
if (rows.length > 0) {
|
|
@@ -8372,10 +8126,10 @@ async function ga4Routes(app, opts) {
|
|
|
8372
8126
|
}
|
|
8373
8127
|
}
|
|
8374
8128
|
tx.delete(gaAiReferrals).where(
|
|
8375
|
-
|
|
8129
|
+
and5(
|
|
8376
8130
|
eq16(gaAiReferrals.projectId, project.id),
|
|
8377
|
-
|
|
8378
|
-
|
|
8131
|
+
sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
8132
|
+
sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
8379
8133
|
)
|
|
8380
8134
|
).run();
|
|
8381
8135
|
if (aiReferrals.length > 0) {
|
|
@@ -8436,16 +8190,16 @@ async function ga4Routes(app, opts) {
|
|
|
8436
8190
|
}).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).get();
|
|
8437
8191
|
const rows = app.db.select({
|
|
8438
8192
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8439
|
-
sessions:
|
|
8440
|
-
organicSessions:
|
|
8441
|
-
users:
|
|
8442
|
-
}).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(
|
|
8193
|
+
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8194
|
+
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8195
|
+
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8196
|
+
}).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8443
8197
|
const aiReferrals = app.db.select({
|
|
8444
8198
|
source: gaAiReferrals.source,
|
|
8445
8199
|
medium: gaAiReferrals.medium,
|
|
8446
|
-
sessions:
|
|
8447
|
-
users:
|
|
8448
|
-
}).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium).orderBy(
|
|
8200
|
+
sessions: sql4`SUM(${gaAiReferrals.sessions})`,
|
|
8201
|
+
users: sql4`SUM(${gaAiReferrals.users})`
|
|
8202
|
+
}).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
|
|
8449
8203
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8450
8204
|
return {
|
|
8451
8205
|
totalSessions: summary?.totalSessions ?? 0,
|
|
@@ -8477,10 +8231,10 @@ async function ga4Routes(app, opts) {
|
|
|
8477
8231
|
}
|
|
8478
8232
|
const trafficPages = app.db.select({
|
|
8479
8233
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8480
|
-
sessions:
|
|
8481
|
-
organicSessions:
|
|
8482
|
-
users:
|
|
8483
|
-
}).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(
|
|
8234
|
+
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8235
|
+
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8236
|
+
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8237
|
+
}).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
8484
8238
|
return {
|
|
8485
8239
|
pages: trafficPages.map((r) => ({
|
|
8486
8240
|
landingPage: r.landingPage,
|
|
@@ -12186,7 +11940,7 @@ import crypto18 from "crypto";
|
|
|
12186
11940
|
import fs4 from "fs";
|
|
12187
11941
|
import path5 from "path";
|
|
12188
11942
|
import os4 from "os";
|
|
12189
|
-
import { and as
|
|
11943
|
+
import { and as and6, eq as eq17, inArray as inArray3 } from "drizzle-orm";
|
|
12190
11944
|
|
|
12191
11945
|
// src/logger.ts
|
|
12192
11946
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -12336,7 +12090,7 @@ var JobRunner = class {
|
|
|
12336
12090
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
12337
12091
|
}
|
|
12338
12092
|
if (existingRun.status === "queued") {
|
|
12339
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
12093
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and6(eq17(runs.id, runId), eq17(runs.status, "queued"))).run();
|
|
12340
12094
|
}
|
|
12341
12095
|
this.throwIfRunCancelled(runId);
|
|
12342
12096
|
const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
|
|
@@ -12787,7 +12541,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
|
|
|
12787
12541
|
|
|
12788
12542
|
// src/gsc-sync.ts
|
|
12789
12543
|
import crypto19 from "crypto";
|
|
12790
|
-
import { eq as eq18, and as
|
|
12544
|
+
import { eq as eq18, and as and7, sql as sql5 } from "drizzle-orm";
|
|
12791
12545
|
var log2 = createLogger("GscSync");
|
|
12792
12546
|
function formatDate2(d) {
|
|
12793
12547
|
return d.toISOString().split("T")[0];
|
|
@@ -12839,10 +12593,10 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
12839
12593
|
});
|
|
12840
12594
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
12841
12595
|
db.delete(gscSearchData).where(
|
|
12842
|
-
|
|
12596
|
+
and7(
|
|
12843
12597
|
eq18(gscSearchData.projectId, projectId),
|
|
12844
|
-
|
|
12845
|
-
|
|
12598
|
+
sql5`${gscSearchData.date} >= ${startDate}`,
|
|
12599
|
+
sql5`${gscSearchData.date} <= ${endDate}`
|
|
12846
12600
|
)
|
|
12847
12601
|
).run();
|
|
12848
12602
|
const batchSize = 500;
|
|
@@ -12928,7 +12682,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
12928
12682
|
}
|
|
12929
12683
|
}
|
|
12930
12684
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
12931
|
-
db.delete(gscCoverageSnapshots).where(
|
|
12685
|
+
db.delete(gscCoverageSnapshots).where(and7(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
12932
12686
|
db.insert(gscCoverageSnapshots).values({
|
|
12933
12687
|
id: crypto19.randomUUID(),
|
|
12934
12688
|
projectId,
|
|
@@ -12951,7 +12705,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
12951
12705
|
|
|
12952
12706
|
// src/gsc-inspect-sitemap.ts
|
|
12953
12707
|
import crypto20 from "crypto";
|
|
12954
|
-
import { eq as eq19, and as
|
|
12708
|
+
import { eq as eq19, and as and8 } from "drizzle-orm";
|
|
12955
12709
|
|
|
12956
12710
|
// src/sitemap-parser.ts
|
|
12957
12711
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -13115,7 +12869,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13115
12869
|
}
|
|
13116
12870
|
}
|
|
13117
12871
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13118
|
-
db.delete(gscCoverageSnapshots).where(
|
|
12872
|
+
db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13119
12873
|
db.insert(gscCoverageSnapshots).values({
|
|
13120
12874
|
id: crypto20.randomUUID(),
|
|
13121
12875
|
projectId,
|
|
@@ -13308,7 +13062,7 @@ var Scheduler = class {
|
|
|
13308
13062
|
};
|
|
13309
13063
|
|
|
13310
13064
|
// src/notifier.ts
|
|
13311
|
-
import { eq as eq21, desc as desc7, and as
|
|
13065
|
+
import { eq as eq21, desc as desc7, and as and9, or as or2 } from "drizzle-orm";
|
|
13312
13066
|
import crypto21 from "crypto";
|
|
13313
13067
|
var log5 = createLogger("Notifier");
|
|
13314
13068
|
var Notifier = class {
|
|
@@ -13372,7 +13126,7 @@ var Notifier = class {
|
|
|
13372
13126
|
}
|
|
13373
13127
|
computeTransitions(runId, projectId) {
|
|
13374
13128
|
const recentRuns = this.db.select().from(runs).where(
|
|
13375
|
-
|
|
13129
|
+
and9(
|
|
13376
13130
|
eq21(runs.projectId, projectId),
|
|
13377
13131
|
or2(eq21(runs.status, "completed"), eq21(runs.status, "partial"))
|
|
13378
13132
|
)
|