Skip to content

Commit 065d766

Browse files
rsslldnphyclaude
andcommitted
Add custom error types for catchable ORM failures
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 065d766

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)