Skip to content

Commit dcf72df

Browse files
author
Georg Traar
committed
Add better error handling
1 parent 0ec5906 commit dcf72df

File tree

9 files changed

+626
-453
lines changed

9 files changed

+626
-453
lines changed

package-lock.json

Lines changed: 237 additions & 149 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/CrateDBClient.ts

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
CrateDBBaseResponse,
1010
CrateDBResponse,
1111
CrateDBBulkResponse,
12-
CrateDBBulkRecord,
1312
CrateDBRecord,
13+
CrateDBErrorResponse,
1414
} from './interfaces';
15+
import { CrateDBError, DeserializationError, RequestError } from './utils/Error.js';
1516

1617
// Configuration options with CrateDB-specific environment variables
1718
const defaultConfig: CrateDBConfig = {
@@ -105,43 +106,60 @@ export class CrateDBClient {
105106
}
106107
}
107108

108-
async execute(stmt: string, args: unknown[] = []): Promise<CrateDBResponse> {
109-
return await this._execute(stmt, args);
110-
}
109+
async execute(stmt: string, args?: unknown[]): Promise<CrateDBResponse> {
110+
const startRequestTime = Date.now();
111+
const payload = args ? { stmt, args } : { stmt };
112+
let body: string;
113+
try {
114+
body = Serializer.serialize(payload);
115+
} catch (serializationError: unknown) {
116+
const msg = serializationError instanceof Error ? serializationError.message : String(serializationError);
117+
throw new RequestError(`Serialization failed: ${msg}`);
118+
}
111119

112-
async executeMany(stmt: string, bulk_args: unknown[][]): Promise<CrateDBBulkResponse> {
113-
const res: CrateDBBulkResponse = await this._execute(stmt, null, bulk_args);
114-
const results: Array<CrateDBBulkRecord> = res.results || [];
115-
const bulk_errors = results.map((result, i) => (result.rowcount === -2 ? i : null)).filter((i) => i !== null);
120+
const options = { ...this.httpOptions, body };
116121

117-
if (bulk_errors.length > 0) {
118-
res.bulk_errors = bulk_errors;
122+
try {
123+
const response = await this._makeRequest(options);
124+
return this._addDurations(startRequestTime, response) as CrateDBResponse;
125+
} catch (error: unknown) {
126+
if (error instanceof CrateDBError || error instanceof DeserializationError) {
127+
throw error;
128+
} else if (error instanceof Error) {
129+
throw new RequestError(`CrateDB request failed: ${error.message}`, { cause: error });
130+
}
131+
throw new RequestError('CrateDB request failed with an unknown error');
119132
}
120-
return res;
121133
}
122134

123-
private async _execute(
124-
stmt: string,
125-
args: unknown[] | null = null,
126-
bulk_args: unknown[][] | null = null
127-
): Promise<CrateDBBaseResponse> {
135+
async executeMany(stmt: string, bulk_args: unknown[][]): Promise<CrateDBBulkResponse> {
128136
const startRequestTime = Date.now();
129-
const body = Serializer.serialize(args ? { stmt, args } : { stmt, bulk_args });
137+
let body: string;
138+
try {
139+
body = Serializer.serialize({ stmt, bulk_args });
140+
} catch (serializationError: unknown) {
141+
const msg = serializationError instanceof Error ? serializationError.message : String(serializationError);
142+
throw new RequestError(`Serialization failed: ${msg}`);
143+
}
144+
130145
const options = { ...this.httpOptions, body };
131-
const response = await this._makeRequest(options);
132-
const totalRequestTime = Date.now() - startRequestTime;
133-
if (typeof response.duration === 'number') {
134-
response.durations = {
135-
cratedb: response.duration,
136-
request: totalRequestTime - response.duration,
137-
};
138-
} else {
139-
response.durations = {
140-
cratedb: 0,
141-
request: totalRequestTime,
142-
};
146+
147+
try {
148+
const response = await this._makeRequest(options);
149+
const res = this._addDurations(startRequestTime, response) as CrateDBBulkResponse;
150+
// Mark bulk errors for each result where rowcount is -2
151+
res.bulk_errors = (res.results || [])
152+
.map((result, i) => (result.rowcount === -2 ? i : null))
153+
.filter((i) => i !== null);
154+
return res;
155+
} catch (error: unknown) {
156+
if (error instanceof CrateDBError || error instanceof DeserializationError) {
157+
throw error;
158+
} else if (error instanceof Error) {
159+
throw new RequestError(`CrateDB bulk request failed: ${error.message}`, { cause: error });
160+
}
161+
throw new RequestError('CrateDB bulk request failed with an unknown error');
143162
}
144-
return response;
145163
}
146164

147165
// Convenience methods for common SQL operations
@@ -182,7 +200,7 @@ export class CrateDBClient {
182200
const args = Object.values(obj);
183201

184202
// Execute the query
185-
return await this.execute(query, args);
203+
return this.execute(query, args);
186204
}
187205

188206
async insertMany(
@@ -229,22 +247,22 @@ export class CrateDBClient {
229247
const { keys, values, args } = this._prepareOptions(options);
230248
const setClause = keys.map((key, i) => `${key}=${values[i]}`).join(', ');
231249
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`;
232-
return await this.execute(query, args);
250+
return this.execute(query, args);
233251
}
234252

235253
async delete(tableName: string, whereClause: string): Promise<CrateDBResponse> {
236254
const query = `DELETE FROM ${tableName} WHERE ${whereClause}`;
237-
return await this.execute(query);
255+
return this.execute(query);
238256
}
239257

240258
async drop(tableName: string): Promise<CrateDBResponse> {
241259
const query = `DROP TABLE IF EXISTS ${tableName}`;
242-
return await this.execute(query);
260+
return this.execute(query);
243261
}
244262

245263
async refresh(tableName: string): Promise<CrateDBResponse> {
246264
const query = `REFRESH TABLE ${tableName}`;
247-
return await this.execute(query);
265+
return this.execute(query);
248266
}
249267

250268
async createTable(schema: Record<string, Record<string, string>>): Promise<CrateDBResponse> {
@@ -253,7 +271,7 @@ export class CrateDBClient {
253271
.map(([col, type]) => `"${col}" ${type}`)
254272
.join(', ');
255273
const query = `CREATE TABLE ${tableName} (${columns})`;
256-
return await this.execute(query);
274+
return this.execute(query);
257275
}
258276

259277
private _prepareOptions(options: Record<string, unknown>): {
@@ -267,6 +285,22 @@ export class CrateDBClient {
267285
return { keys, values, args };
268286
}
269287

288+
private _addDurations(startRequestTime: number, response: CrateDBBaseResponse): CrateDBBaseResponse {
289+
const totalRequestTime = Date.now() - startRequestTime;
290+
if (typeof response.duration === 'number') {
291+
response.durations = {
292+
cratedb: response.duration,
293+
request: totalRequestTime - response.duration,
294+
};
295+
} else {
296+
response.durations = {
297+
cratedb: 0,
298+
request: totalRequestTime,
299+
};
300+
}
301+
return response;
302+
}
303+
270304
async _makeRequest(options: http.RequestOptions & { body?: string }): Promise<CrateDBBaseResponse> {
271305
return new Promise((resolve, reject) => {
272306
const requestBodySize = options.body ? Buffer.byteLength(options.body) : 0;
@@ -276,18 +310,19 @@ export class CrateDBClient {
276310
response.on('end', () => {
277311
const rawResponse = Buffer.concat(data); // Raw response data as a buffer
278312
const responseBodySize = rawResponse.length;
313+
279314
try {
280315
const parsedResponse = Serializer.deserialize(rawResponse.toString(), this.cfg.deserialization);
281-
resolve({
282-
...parsedResponse,
283-
sizes: { response: responseBodySize, request: requestBodySize },
284-
});
285-
} catch (parseErr: unknown) {
286-
if (response.statusCode === 401) {
287-
reject(new Error('Authentication error: Invalid credentials or insufficient permissions.'));
288-
} else if (response.statusCode === 503) {
289-
reject(new Error('Service unavailable: server is not available (503).'));
316+
if (response.statusCode === 200) {
317+
resolve({
318+
...parsedResponse,
319+
sizes: { response: responseBodySize, request: requestBodySize },
320+
});
321+
} else {
322+
reject(CrateDBError.fromResponse(parsedResponse as CrateDBErrorResponse, response.statusCode));
290323
}
324+
} catch (parseErr: unknown) {
325+
// Handle parsing errors
291326
if (parseErr instanceof Error) {
292327
reject(
293328
new Error(`Failed to parse response: ${parseErr.message}. Raw response: ${rawResponse.toString()}`)
@@ -302,6 +337,7 @@ export class CrateDBClient {
302337
req.end(options.body || null);
303338
});
304339
}
340+
305341
public getConfig(): Readonly<CrateDBConfig> {
306342
return this.cfg;
307343
}

src/Cursor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class Cursor {
8989
async _execute(sql: string): Promise<Array<CrateDBRecord>> {
9090
const options = { ...this.connectionOptions, body: Serializer.serialize({ stmt: sql }) };
9191
try {
92-
const response: CrateDBResponse = await this.client._makeRequest(options);
92+
const response: CrateDBResponse = (await this.client._makeRequest(options)) as CrateDBResponse;
9393
const { cols, rows, rowcount } = response;
9494
return rowcount && rowcount > 0 ? this._rebuildObjects(cols || [], rows as unknown[]) : [];
9595
} catch (error: unknown) {

src/Serializer.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { ColumnTypes } from './ColumnTypes.js';
1+
import { ColumnTypes } from './utils/ColumnTypes.js';
22
import { CrateDBBaseResponse, DeserializationConfig } from './interfaces.js';
3+
import { DeserializationError } from './utils/Error.js';
34

45
type Context = {
56
source: string;
@@ -24,6 +25,14 @@ export class Serializer {
2425
}
2526

2627
static deserialize(str: string, config: DeserializationConfig): CrateDBBaseResponse {
28+
try {
29+
return this._deserialize(str, config);
30+
} catch {
31+
throw new DeserializationError('Deserialization of response body failed');
32+
}
33+
}
34+
35+
private static _deserialize(str: string, config: DeserializationConfig): CrateDBBaseResponse {
2736
const obj = config.long === 'bigint' ? JSON.parse(str, this.reviver) : JSON.parse(str);
2837

2938
obj.col_types?.forEach((type: number | number[], index: number) => {

src/interfaces.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,21 @@ export interface CrateDBBaseResponse {
3333
response: number;
3434
};
3535
error?: { code: number; message: string };
36+
results?: Array<CrateDBBulkRecord>;
3637
}
3738

3839
export interface CrateDBResponse extends CrateDBBaseResponse {
39-
rows?: Array<Array<unknown>>;
40-
rowcount?: number;
40+
cols: string[];
41+
col_types: number[];
42+
rows: unknown[][];
43+
rowcount: number;
44+
duration: number;
4145
}
4246

4347
export interface CrateDBBulkResponse extends CrateDBBaseResponse {
44-
results?: Array<CrateDBBulkRecord>;
48+
results: Array<CrateDBBulkRecord>;
4549
bulk_errors?: number[];
50+
duration: number;
4651
}
4752

4853
export interface CrateDBBulkRecord {
@@ -53,4 +58,12 @@ export interface CrateDBBulkRecord {
5358
};
5459
}
5560

61+
export interface CrateDBErrorResponse {
62+
error: {
63+
message: string;
64+
code: number;
65+
};
66+
error_trace?: string;
67+
}
68+
5669
export type CrateDBRecord = Record<string, unknown>;

src/utils/Error.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CrateDBErrorResponse } from '../interfaces';
2+
3+
export class CrateDBError extends Error {
4+
constructor(
5+
message: string,
6+
public readonly code: number,
7+
public readonly error_trace?: string,
8+
public readonly statusCode?: number
9+
) {
10+
super(message);
11+
this.name = 'CrateDBError';
12+
Object.setPrototypeOf(this, CrateDBError.prototype);
13+
}
14+
15+
static fromResponse(response: CrateDBErrorResponse, statusCode?: number): CrateDBError {
16+
return new CrateDBError(response.error.message, response.error.code, response.error_trace, statusCode);
17+
}
18+
}
19+
20+
export class DeserializationError extends Error {
21+
constructor(message = 'Deserialization failed', options?: { cause?: Error }) {
22+
super(message, options);
23+
this.name = 'DeserializationError';
24+
Object.setPrototypeOf(this, DeserializationError.prototype);
25+
}
26+
}
27+
28+
export class RequestError extends Error {
29+
constructor(message: string, options?: { cause?: Error }) {
30+
super(message, options);
31+
this.name = 'RequestError';
32+
Object.setPrototypeOf(this, RequestError.prototype);
33+
}
34+
}

0 commit comments

Comments
 (0)