@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.
@@ -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, and } from "drizzle-orm";
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
- const err = validationError("Invalid project payload", {
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
- const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
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 ? JSON.parse(existing.locations || "[]") : [];
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
- const err = validationError(`Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`, {
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
- const err = validationError(`defaultLocation "${nextDefaultLocation}" must match a configured location label`, {
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
- try {
1936
- const project = resolveProject(app.db, request.params.name);
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
- let project;
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
- let project;
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
- const err = validationError(parsed.error.issues.map((i) => i.message).join(", "));
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 = JSON.parse(project.locations || "[]");
1967
+ const existing = parseJsonColumn(project.locations, []);
1986
1968
  if (existing.some((l) => l.label === location.label)) {
1987
- const err = validationError(`Location "${location.label}" already exists`);
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
- let project;
2007
- try {
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
- let project;
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 = JSON.parse(project.locations || "[]");
1997
+ const existing = parseJsonColumn(project.locations, []);
2035
1998
  const filtered = existing.filter((l) => l.label !== label);
2036
1999
  if (filtered.length === existing.length) {
2037
- const err = validationError(`Location "${label}" not found`);
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
- let project;
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
- const err = validationError("label is required");
2072
- return reply.status(err.statusCode).send(err.toJSON());
2024
+ throw validationError("label is required");
2073
2025
  }
2074
- const existing = JSON.parse(project.locations || "[]");
2026
+ const existing = parseJsonColumn(project.locations, []);
2075
2027
  if (!existing.some((l) => l.label === label)) {
2076
- const err = validationError(`Location "${label}" not found. Add it first.`);
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
- let project;
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: JSON.parse(project.labels)
2055
+ labels: parseJsonColumn(project.labels, {})
2114
2056
  },
2115
2057
  spec: {
2116
2058
  displayName: project.displayName,
2117
2059
  canonicalDomain: project.canonicalDomain,
2118
- ownedDomains: JSON.parse(project.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: JSON.parse(project.providers || "[]"),
2124
- locations: JSON.parse(project.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 = JSON.parse(row.config);
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: JSON.parse(schedule.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: JSON.parse(row.ownedDomains || "[]"),
2094
+ ownedDomains: parseJsonColumn(row.ownedDomains, []),
2153
2095
  country: row.country,
2154
2096
  language: row.language,
2155
- tags: JSON.parse(row.tags),
2156
- labels: JSON.parse(row.labels),
2157
- providers: JSON.parse(row.providers || "[]"),
2158
- locations: JSON.parse(row.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 = resolveProjectSafe(app, request.params.name, reply);
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 = resolveProjectSafe(app, request.params.name, reply);
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
- const err = validationError('Body must contain a "keywords" array');
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 = resolveProjectSafe(app, request.params.name, reply);
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
- const err = validationError('Body must contain a non-empty "keywords" array');
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 = resolveProjectSafe(app, request.params.name, reply);
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
- const err = validationError('Body must contain a "keywords" array');
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 = resolveProjectSafe(app, request.params.name, reply);
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
- const err = validationError('Body must contain a "provider" string');
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
- const err = validationError(`Unknown provider "${body.provider}". Valid providers: ${validNames.join(", ")}`, {
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
- const err = validationError('"count" must be an integer');
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
- const err = notImplemented("Key phrase generation is not supported in this deployment");
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 = resolveProjectSafe2(app, request.params.name, reply);
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 = resolveProjectSafe2(app, request.params.name, reply);
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
- const err = validationError('Body must contain a "competitors" array');
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 as and2, eq as eq6, or } from "drizzle-orm";
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
- and2(
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 = resolveProjectSafe3(app, request.params.name, reply);
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
- const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
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 = JSON.parse(project.locations || "[]");
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
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: `Location "${request.body.location}" not found. Configure it first.` } });
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
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "No locations configured for this project" } });
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 = resolveProjectSafe3(app, request.params.name, reply);
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
- const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
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: tryParseJson(s.citedDomains, []),
2648
- competitorOverlap: tryParseJson(s.competitorOverlap, []),
2649
- recommendedCompetitors: tryParseJson(s.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 = tryParseJson(raw ?? "{}", {});
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
- const err = validationError("Invalid project config", {
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
- const err = validationError(`Invalid provider(s): ${[...new Set(invalid)].join(", ")}. Must be one of: ${validNames.join(", ")}`, {
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
- if (existing) {
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
- if (config.spec.schedule) {
3071
- const schedSpec = config.spec.schedule;
3072
- let cronExpr;
3073
- let preset = null;
3074
- if (schedSpec.preset) {
3075
- preset = schedSpec.preset;
3076
- try {
3077
- cronExpr = resolvePreset(schedSpec.preset);
3078
- } catch (err) {
3079
- const msg = err instanceof Error ? err.message : String(err);
3080
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: msg } });
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
- } else if (schedSpec.cron) {
3083
- cronExpr = schedSpec.cron;
3084
- if (!validateCron(cronExpr)) {
3085
- return reply.status(400).send({
3086
- error: { code: "VALIDATION_ERROR", message: `Invalid cron expression in schedule: ${cronExpr}` }
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
- const existingSched = app.db.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
3101
- if (existingSched) {
3102
- app.db.update(schedules).set({
3103
- cronExpr,
3104
- preset,
3105
- timezone,
3106
- providers: JSON.stringify(schedSpec.providers ?? []),
3107
- enabled: 1,
3108
- updatedAt: now
3109
- }).where(eq8(schedules.id, existingSched.id)).run();
3110
- } else {
3111
- app.db.insert(schedules).values({
3112
- id: crypto10.randomUUID(),
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
- channel: notif.channel,
3147
- config: JSON.stringify({ url: notif.url, events: notif.events }),
3148
- webhookSecret: crypto10.randomBytes(32).toString("hex"),
3149
- enabled: 1,
3150
- createdAt: now,
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
- writeAuditLog(app.db, {
3155
- projectId,
3156
- actor: "api",
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: JSON.parse(project.ownedDomains || "[]"),
3032
+ ownedDomains: parseJsonColumn(project.ownedDomains, []),
3172
3033
  country: project.country,
3173
3034
  language: project.language,
3174
- tags: JSON.parse(project.tags),
3175
- labels: JSON.parse(project.labels),
3176
- providers: JSON.parse(project.providers || "[]"),
3177
- locations: JSON.parse(project.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 = resolveProjectSafe4(app, request.params.name, reply);
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 = resolveProjectSafe4(app, request.params.name, reply);
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: tryParseJson2(s.citedDomains, []),
3281
- competitorOverlap: tryParseJson2(s.competitorOverlap, []),
3282
- recommendedCompetitors: tryParseJson2(s.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 = resolveProjectSafe4(app, request.params.name, reply);
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
- const project = resolveProjectSafe4(app, request.params.name, reply);
3374
- if (!project) return;
3231
+ resolveProject(app.db, request.params.name);
3375
3232
  const { run1, run2 } = request.query;
3376
3233
  if (!run1 || !run2) {
3377
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "Both run1 and run2 query params are required" } });
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(tryParseJson2(row.diff, null)) : tryParseJson2(row.diff, null) : null,
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 = resolveProjectSafe5(app, request.params.name, reply);
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 = resolveProjectSafe5(app, request.params.name, reply);
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 = tryParseJson3(s.competitorOverlap, []);
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 = resolveProjectSafe5(app, request.params.name, reply);
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 = tryParseJson3(rawResponse, {});
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 = resolveProjectSafe6(app, request.params.name, reply);
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
- const err = validationError("Invalid schedule payload", {
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
- const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
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
- return reply.status(400).send({
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
- return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: msg } });
5862
+ throw validationError(msg);
6053
5863
  }
6054
5864
  } else {
6055
5865
  cronExpr = cron2;
6056
5866
  if (!validateCron(cronExpr)) {
6057
- return reply.status(400).send({
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 = resolveProjectSafe6(app, request.params.name, reply);
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
- return reply.status(404).send({ error: { code: "NOT_FOUND", message: `No schedule for project '${request.params.name}'` } });
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 = resolveProjectSafe6(app, request.params.name, reply);
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
- return reply.status(404).send({ error: { code: "NOT_FOUND", message: `No schedule for project '${request.params.name}'` } });
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: JSON.parse(row.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 = resolveProjectSafe7(app, request.params.name, reply);
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
- return reply.status(400).send({
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
- return reply.status(400).send({
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 = resolveProjectSafe7(app, request.params.name, reply);
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 = resolveProjectSafe7(app, request.params.name, reply);
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
- return reply.status(404).send({
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 = resolveProjectSafe7(app, request.params.name, reply);
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
- return reply.status(404).send({
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 = JSON.parse(notification.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 = JSON.parse(row.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 and3, desc as desc4, sql as sql2 } from "drizzle-orm";
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(sql2`${gscSearchData.date} >= ${startDate}`);
6762
- if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
6763
- if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
6764
- if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
6765
- const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
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(and3(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
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 and4, desc as desc5 } from "drizzle-orm";
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 ? and4(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
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 and5 } from "drizzle-orm";
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(and5(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
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 and6, sql as sql3 } from "drizzle-orm";
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
- and6(
8108
+ and5(
8355
8109
  eq16(gaTrafficSnapshots.projectId, project.id),
8356
- sql3`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8357
- sql3`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
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
- and6(
8129
+ and5(
8376
8130
  eq16(gaAiReferrals.projectId, project.id),
8377
- sql3`${gaAiReferrals.date} >= ${summary.periodStart}`,
8378
- sql3`${gaAiReferrals.date} <= ${summary.periodEnd}`
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: sql3`SUM(${gaTrafficSnapshots.sessions})`,
8440
- organicSessions: sql3`SUM(${gaTrafficSnapshots.organicSessions})`,
8441
- users: sql3`SUM(${gaTrafficSnapshots.users})`
8442
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql3`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
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: sql3`SUM(${gaAiReferrals.sessions})`,
8447
- users: sql3`SUM(${gaAiReferrals.users})`
8448
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium).orderBy(sql3`SUM(${gaAiReferrals.sessions}) DESC`).all();
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: sql3`SUM(${gaTrafficSnapshots.sessions})`,
8481
- organicSessions: sql3`SUM(${gaTrafficSnapshots.organicSessions})`,
8482
- users: sql3`SUM(${gaTrafficSnapshots.users})`
8483
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql3`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
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 and7, eq as eq17, inArray as inArray3 } from "drizzle-orm";
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(and7(eq17(runs.id, runId), eq17(runs.status, "queued"))).run();
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 and8, sql as sql4 } from "drizzle-orm";
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
- and8(
12596
+ and7(
12843
12597
  eq18(gscSearchData.projectId, projectId),
12844
- sql4`${gscSearchData.date} >= ${startDate}`,
12845
- sql4`${gscSearchData.date} <= ${endDate}`
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(and8(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
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 and9 } from "drizzle-orm";
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(and9(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
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 and10, or as or2 } from "drizzle-orm";
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
- and10(
13129
+ and9(
13376
13130
  eq21(runs.projectId, projectId),
13377
13131
  or2(eq21(runs.status, "completed"), eq21(runs.status, "partial"))
13378
13132
  )