Browse Source

feat: on db error respond with human-readable message

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5174/head
Pranav C 2 years ago
parent
commit
fae02790b3
  1. 386
      packages/nocodb/src/lib/meta/helpers/catchError.ts

386
packages/nocodb/src/lib/meta/helpers/catchError.ts

@ -1,7 +1,369 @@
enum DBError {
TABLE_EXIST = 'TABLE_EXIST',
TABLE_NOT_EXIST = 'TABLE_NOT_EXIST',
COLUMN_EXIST = 'COLUMN_EXIST',
COLUMN_NOT_EXIST = 'COLUMN_NOT_EXIST',
CONSTRAINT_EXIST = 'CONSTRAINT_EXIST',
CONSTRAINT_NOT_EXIST = 'CONSTRAINT_NOT_EXIST',
}
// extract db errors using database error code
function extractDBError(error): {
type: DBError;
message: string;
info: any;
extra?: Record<string, any>;
} | void {
if (!error.code) return;
let message: string;
let extra: Record<string, any>;
let type: DBError;
switch (error.code) {
// sqlite errors
case 'SQLITE_BUSY':
message = 'The database is locked by another process or transaction.';
break;
case 'SQLITE_CONSTRAINT':
{
const constraint = /FOREIGN KEY|UNIQUE/.test(error.message)
? error.message.match(/FOREIGN KEY|UNIQUE/gi)?.join(' ')
: 'constraint';
message = `A ${constraint} constraint was violated: ${error.message}`;
extra = {
constraint,
};
}
break;
case 'SQLITE_CORRUPT':
message = 'The database file is corrupt.';
break;
case 'SQLITE_ERROR':
message = 'A SQL error occurred.';
if (error.message) {
const noSuchTableMatch = error.message.match(/no such table: (\w+)/);
const tableAlreadyExistsMatch = error.message.match(
/SQLITE_ERROR: table `?(\w+)`? already exists/
);
const duplicateColumnExistsMatch = error.message.match(
/SQLITE_ERROR: duplicate column name: (\w+)/
);
const unrecognizedTokenMatch = error.message.match(
/SQLITE_ERROR: unrecognized token: "(\w+)"/
);
const columnDoesNotExistMatch = error.message.match(
/SQLITE_ERROR: no such column: (\w+)/
);
const constraintFailedMatch = error.message.match(
/SQLITE_ERROR: constraint failed: (\w+)/
);
function extractDBError(_error:Error) {
// if (error instanceof knex.DBError) { switch (error.code) { case 'ER_NO_SUCH_TABLE': console.error('Table does not exist'); break; case 'ER_BAD_FIELD_ERROR': console.error('Invalid column name'); break; // Add more cases for other common errors here default: console.error('Unknown error:', error); break; } } else { console.error('Unexpected error:', error); } }).finally(() => { knex.destroy(); // Close the database connection });
if (noSuchTableMatch && noSuchTableMatch[1]) {
message = `The table '${noSuchTableMatch[1]}' does not exist.`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: noSuchTableMatch[1],
};
} else if (tableAlreadyExistsMatch && tableAlreadyExistsMatch[1]) {
message = `The table '${tableAlreadyExistsMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: tableAlreadyExistsMatch[1],
};
} else if (unrecognizedTokenMatch && unrecognizedTokenMatch[1]) {
message = `Unrecognized token: ${unrecognizedTokenMatch[1]}`;
extra = {
token: unrecognizedTokenMatch[1],
};
} else if (columnDoesNotExistMatch && columnDoesNotExistMatch[1]) {
message = `The column ${columnDoesNotExistMatch[1]} does not exist.`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: columnDoesNotExistMatch[1],
};
} else if (constraintFailedMatch && constraintFailedMatch[1]) {
message = `A constraint failed: ${constraintFailedMatch[1]}`;
} else if (
duplicateColumnExistsMatch &&
duplicateColumnExistsMatch[1]
) {
message = `The column '${duplicateColumnExistsMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: duplicateColumnExistsMatch[1],
};
} else {
const match = error.message.match(/SQLITE_ERROR:\s*(\w+)/);
if (match && match[1]) {
message = match[1];
}
}
}
break;
case 'SQLITE_RANGE':
message = 'A column index is out of range.';
break;
case 'SQLITE_SCHEMA':
message = 'The database schema has changed.';
break;
// mysql errors
case 'ER_TABLE_EXISTS_ERROR':
message = 'The table already exists.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ Table '?(\w+)'? already exists/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: extractTableNameMatch[1],
};
}
}
break;
case 'ER_DUP_FIELDNAME':
message = 'The column already exists.';
if (error.message) {
const extractColumnNameMatch = error.message.match(
/ Duplicate column name '(\w+)'/i
);
if (extractColumnNameMatch && extractColumnNameMatch[1]) {
message = `The column '${extractColumnNameMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: extractColumnNameMatch[1],
};
}
}
break;
case 'ER_NO_SUCH_TABLE':
message = 'The table does not exist.';
if (error.message) {
const missingTableMatch = error.message.match(
/ Table '(?:\w+\.)?(\w+)' doesn't exist/i
);
if (missingTableMatch && missingTableMatch[1]) {
message = `The table '${missingTableMatch[1]}' does not exist`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: missingTableMatch[1],
};
}
}
break;
case 'ER_DUP_ENTRY':
message = 'This record already exists.';
break;
case 'ER_PARSE_ERROR':
message = 'There was a syntax error in your SQL query.';
break;
case 'ER_NO_DEFAULT_FOR_FIELD':
message = 'A value is required for this field.';
break;
case 'ER_BAD_NULL_ERROR':
message = 'A null value is not allowed for this field.';
break;
case 'ER_DATA_TOO_LONG':
message = 'The data entered is too long for this field.';
break;
case 'ER_BAD_FIELD_ERROR':
{
message = 'The field you are trying to access does not exist.';
const extractColNameMatch = error.message.match(
/ Unknown column '(\w+)' in 'field list'/i
);
if (extractColNameMatch && extractColNameMatch[1]) {
message = `The column '${extractColNameMatch[1]}' does not exist.`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: extractColNameMatch[1],
};
}
}
break;
case 'ER_ACCESS_DENIED_ERROR':
message = 'You do not have permission to perform this action.';
break;
case 'ER_LOCK_WAIT_TIMEOUT':
message = 'A timeout occurred while waiting for a table lock.';
break;
case 'ER_NO_REFERENCED_ROW':
message = 'The referenced row does not exist.';
break;
case 'ER_ROW_IS_REFERENCED':
message = 'This record is being referenced by other records.';
break;
// postgres errors
case '23505':
message = 'This record already exists.';
break;
case '42601':
message = 'There was a syntax error in your SQL query.';
break;
case '23502':
message = 'A value is required for this field.';
break;
case '23503':
message = 'The referenced row does not exist.';
break;
case '23514':
message = 'A null value is not allowed for this field.';
break;
case '22001':
message = 'The data entered is too long for this field.';
break;
case '28000':
message = 'You do not have permission to perform this action.';
break;
case '40P01':
message = 'A timeout occurred while waiting for a table lock.';
break;
case '23506':
message = 'This record is being referenced by other records.';
break;
case '42P07':
message = 'The table already exists.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ relation "?(\w+)"? already exists/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: extractTableNameMatch[1],
};
}
}
break;
case '42701':
message = 'The column already exists.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ column "(\w+)" of relation "(\w+)" already exists/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The column '${extractTableNameMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: extractTableNameMatch[1],
};
}
}
break;
case '42P01':
message = 'The table does not exist.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ relation "(\w+)" does not exist/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' does not exist.`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: extractTableNameMatch[1],
};
}
}
break;
case '42703':
message = 'The column does not exist.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ column "(\w+)" does not exist/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The column '${extractTableNameMatch[1]}' does not exist.`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: extractTableNameMatch[1],
};
}
}
break;
// mssql errors
case 'EREQUEST':
message = 'There was a syntax error in your SQL query.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ There is already an object named '(\w+)' in the database/i
);
const extractDupColMatch = error.message.match(
/ Column name '(\w+)' in table '(\w+)' is specified more than once/i
);
const extractMissingTableMatch = error.message.match(
/ Invalid object name '(\w+)'./i
);
const extractMissingColMatch = error.message.match(
/ Invalid column name '(\w+)'./i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: extractTableNameMatch[1],
};
} else if (extractDupColMatch && extractDupColMatch[1]) {
message = `The column '${extractDupColMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: extractDupColMatch[1],
};
} else if (extractMissingTableMatch && extractMissingTableMatch[1]) {
message = `The table '${extractMissingTableMatch[1]}' does not exist`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: extractMissingTableMatch[1],
};
} else if (extractMissingColMatch && extractMissingColMatch[1]) {
message = `The column '${extractMissingColMatch[1]}' does not exist`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: extractMissingColMatch[1],
};
}
}
break;
case 'ELOGIN':
message = 'You do not have permission to perform this action.';
break;
case 'ETIMEOUT':
message = 'A timeout occurred while waiting for a table lock.';
break;
case 'ECONNRESET':
message = 'The connection was reset.';
break;
case 'ECONNREFUSED':
message = 'The connection was refused.';
break;
case 'EHOSTUNREACH':
message = 'The host is unreachable.';
break;
case 'EHOSTDOWN':
message = 'The host is down.';
break;
}
if (message) {
return {
message,
type,
extra,
info: { message: error.message, code: error.code },
};
}
}
export default function (
@ -11,13 +373,15 @@ export default function (
try {
return await requestHandler(req, res, next);
} catch (e) {
extractDBError(e)
// todo: error log
console.log(requestHandler.name ? `${requestHandler.name} ::` : '', e);
const dbError = extractDBError(e);
if (dbError) {
return res.status(400).json(dbError);
}
if (e instanceof BadRequest) {
return res.status(400).json({ msg: e.message });
} else if (e instanceof Unauthorized) {
@ -37,28 +401,38 @@ export default function (
}
class BadRequest extends Error {}
class Unauthorized extends Error {}
class Forbidden extends Error {}
class NotFound extends Error {}
class InternalServerError extends Error {}
class NotImplemented extends Error {}
export class NcError {
static notFound(message = 'Not found') {
throw new NotFound(message);
}
static badRequest(message) {
throw new BadRequest(message);
}
static unauthorized(message) {
throw new Unauthorized(message);
}
static forbidden(message) {
throw new Forbidden(message);
}
static internalServerError(message = 'Internal server error') {
throw new InternalServerError(message);
}
static notImplemented(message = 'Not implemented') {
throw new NotImplemented(message);
}

Loading…
Cancel
Save