Skip to content

Commit 4b71aa7

Browse files
rsslldnphyclaude
andauthored
Add custom error types for catchable ORM failures (#359)
Introduces an OrmError base class plus NotFoundError, TooManyRowsError, and CreateFailedError so consumers can distinguish runtime row-count failures from generic errors via instanceof. Each carries the modelName and operation that caused it; TooManyRowsError also exposes rowCount. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 550f1a5 commit 4b71aa7

6 files changed

Lines changed: 107 additions & 7 deletions

File tree

packages/orm/src/errors.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export type SingleRowOperation = "findOne" | "updateOne" | "deleteOne";
2+
3+
export class OrmError extends Error {
4+
public override readonly name: string = "OrmError";
5+
6+
constructor(message: string) {
7+
super(message);
8+
Object.setPrototypeOf(this, OrmError.prototype);
9+
}
10+
}
11+
12+
export class NotFoundError extends OrmError {
13+
public override readonly name = "NotFoundError";
14+
public readonly modelName: string;
15+
public readonly operation: SingleRowOperation;
16+
17+
constructor(
18+
message: string,
19+
details: { modelName: string; operation: SingleRowOperation },
20+
) {
21+
super(message);
22+
this.modelName = details.modelName;
23+
this.operation = details.operation;
24+
Object.setPrototypeOf(this, NotFoundError.prototype);
25+
}
26+
}
27+
28+
export class TooManyRowsError extends OrmError {
29+
public override readonly name = "TooManyRowsError";
30+
public readonly modelName: string;
31+
public readonly operation: SingleRowOperation;
32+
public readonly rowCount: number;
33+
34+
constructor(
35+
message: string,
36+
details: {
37+
modelName: string;
38+
operation: SingleRowOperation;
39+
rowCount: number;
40+
},
41+
) {
42+
super(message);
43+
this.modelName = details.modelName;
44+
this.operation = details.operation;
45+
this.rowCount = details.rowCount;
46+
Object.setPrototypeOf(this, TooManyRowsError.prototype);
47+
}
48+
}
49+
50+
export class CreateFailedError extends OrmError {
51+
public override readonly name = "CreateFailedError";
52+
public readonly modelName: string;
53+
54+
constructor(message: string, details: { modelName: string }) {
55+
super(message);
56+
this.modelName = details.modelName;
57+
Object.setPrototypeOf(this, CreateFailedError.prototype);
58+
}
59+
}

packages/orm/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ export * from "./operators.js";
33

44
export { orm, type Orm } from "./orm.js";
55

6+
export {
7+
CreateFailedError,
8+
NotFoundError,
9+
OrmError,
10+
type SingleRowOperation,
11+
TooManyRowsError,
12+
} from "./errors.js";
13+
614
export type {
715
Config,
816
FieldDefinition,

packages/orm/src/orm.createOne.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ModelDefinitions } from "@casekit/orm-schema";
33

44
import { buildCreate } from "./builders/buildCreate.js";
55
import { Connection } from "./connection.js";
6+
import { CreateFailedError } from "./errors.js";
67
import { createToSql } from "./sql/createToSql.js";
78
import { CreateOneParams } from "./types/CreateOneParams.js";
89
import { Middleware } from "./types/Middleware.js";
@@ -31,7 +32,9 @@ export const createOne = async (
3132
const result = await tx.query(statement);
3233

3334
if (!result.rowCount && query.onConflict?.do !== "nothing") {
34-
throw new Error("createOne failed to create a row");
35+
throw new CreateFailedError("createOne failed to create a row", {
36+
modelName,
37+
});
3538
}
3639

3740
await tx.commit();

packages/orm/src/orm.deleteOne.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ModelDefinitions, OperatorDefinitions } from "@casekit/orm-schema";
33

44
import { buildDelete } from "./builders/buildDelete.js";
55
import { Connection } from "./connection.js";
6+
import { NotFoundError, TooManyRowsError } from "./errors.js";
67
import { deleteToSql } from "./sql/deleteToSql.js";
78
import { DeleteParams } from "./types/DeleteParams.js";
89
import { Middleware } from "./types/Middleware.js";
@@ -28,9 +29,19 @@ export const deleteOne = async (
2829
const result = await tx.query(statement);
2930

3031
if (!result.rowCount || result.rowCount === 0) {
31-
throw new Error("Delete one failed to delete a row");
32+
throw new NotFoundError("Delete one failed to delete a row", {
33+
modelName,
34+
operation: "deleteOne",
35+
});
3236
} else if (result.rowCount > 1) {
33-
throw new Error("Delete one would have deleted more than one row");
37+
throw new TooManyRowsError(
38+
"Delete one would have deleted more than one row",
39+
{
40+
modelName,
41+
operation: "deleteOne",
42+
rowCount: result.rowCount,
43+
},
44+
);
3445
}
3546

3647
await tx.commit();

packages/orm/src/orm.findOne.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NormalizedConfig } from "@casekit/orm-config";
22
import { ModelDefinitions, OperatorDefinitions } from "@casekit/orm-schema";
33

44
import { Connection } from "./connection.js";
5+
import { NotFoundError, TooManyRowsError } from "./errors.js";
56
import { findMany } from "./orm.findMany.js";
67
import { FindParams } from "./types/FindParams.js";
78
import { Middleware } from "./types/Middleware.js";
@@ -18,10 +19,17 @@ export const findOne = async (
1819
const results = await findMany(config, conn, middleware, modelName, query);
1920

2021
if (results.length === 0) {
21-
throw new Error("Expected one row, but found none");
22+
throw new NotFoundError("Expected one row, but found none", {
23+
modelName,
24+
operation: "findOne",
25+
});
2226
}
2327
if (results.length > 1) {
24-
throw new Error("Expected one row, but found more");
28+
throw new TooManyRowsError("Expected one row, but found more", {
29+
modelName,
30+
operation: "findOne",
31+
rowCount: results.length,
32+
});
2533
}
2634
return results[0]!;
2735
};

packages/orm/src/orm.updateOne.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ModelDefinitions, OperatorDefinitions } from "@casekit/orm-schema";
33

44
import { buildUpdate } from "./builders/buildUpdate.js";
55
import { Connection } from "./connection.js";
6+
import { NotFoundError, TooManyRowsError } from "./errors.js";
67
import { updateToSql } from "./sql/updateToSql.js";
78
import { Middleware } from "./types/Middleware.js";
89
import { UpdateParams } from "./types/UpdateParams.js";
@@ -32,9 +33,19 @@ export const updateOne = async (
3233
const result = await tx.query(statement);
3334

3435
if (!result.rowCount || result.rowCount === 0) {
35-
throw new Error("Update one failed to update a row");
36+
throw new NotFoundError("Update one failed to update a row", {
37+
modelName,
38+
operation: "updateOne",
39+
});
3640
} else if (result.rowCount > 1) {
37-
throw new Error("Update one would have updated more than one row");
41+
throw new TooManyRowsError(
42+
"Update one would have updated more than one row",
43+
{
44+
modelName,
45+
operation: "updateOne",
46+
rowCount: result.rowCount,
47+
},
48+
);
3849
}
3950

4051
await tx.commit();

0 commit comments

Comments
 (0)