diff --git a/.changeset/poor-cameras-battle.md b/.changeset/poor-cameras-battle.md new file mode 100644 index 00000000..7b852946 --- /dev/null +++ b/.changeset/poor-cameras-battle.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-server': patch +--- + +fix crashes when trying to create a run that already exists diff --git a/packages/log-server/src/app.ts b/packages/log-server/src/app.ts index 5cd89508..15e4d7a4 100644 --- a/packages/log-server/src/app.ts +++ b/packages/log-server/src/app.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/no-namespace */ import express from 'express'; import { zodiosContext } from '@zodios/express'; -import { LogFilter, Store } from './store.js'; +import { LogFilter, Store, StoreError } from './store.js'; import session from 'cookie-session'; -import { SqliteError } from 'better-sqlite3'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { api } from './api.js'; import { csvExportStream, jsonExportStream } from './export.js'; @@ -149,11 +148,10 @@ export function createLogServer({ }, }); } catch (e) { - if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT') { + if (e instanceof StoreError && e.code === 'RUN_EXISTS') { res.status(400).json({ status: 'error', - message: - 'Could not add run, probably because a run with that ID already exists for this experiment', + message: e.message, }); return; } diff --git a/packages/log-server/src/store.ts b/packages/log-server/src/store.ts index b2359827..c17ba1f8 100644 --- a/packages/log-server/src/store.ts +++ b/packages/log-server/src/store.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import * as url from 'node:url'; -import SQliteDB from 'better-sqlite3'; +import SQliteDB, { SqliteError } from 'better-sqlite3'; import { Kysely, FileMigrationProvider, @@ -78,17 +78,30 @@ export class Store { experimentId: string; createdAt: Date; }) { - let result = await this.#db - .insertInto('run') - .values({ - runId, - experimentId, - createdAt: createdAt.toISOString(), - status: 'running', - }) - .returning(['runId', 'experimentId']) - .executeTakeFirstOrThrow(); - return { runId: result.runId, experimentId: result.experimentId }; + try { + let result = await this.#db + .insertInto('run') + .values({ + runId, + experimentId, + createdAt: createdAt.toISOString(), + status: 'running', + }) + .returning(['runId', 'experimentId']) + .executeTakeFirstOrThrow(); + return { runId: result.runId, experimentId: result.experimentId }; + } catch (e) { + if ( + e instanceof SqliteError && + e.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' + ) { + throw new StoreError( + `run "${runId}" already exists for experiment "${experimentId}".`, + 'RUN_EXISTS' + ); + } + throw e; + } } async getRun(experimentId: string, runId: string) { @@ -295,3 +308,15 @@ export type Log = { clientDate?: Date; values: JsonObject; }; + +type StoreErrorCode = 'RUN_EXISTS'; +export class StoreError extends Error { + code: StoreErrorCode; + cause?: Error; + constructor(message: string, code: StoreErrorCode, cause?: Error) { + super(message); + this.name = 'StoreError'; + this.code = code; + this.cause = cause; + } +}