Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for prettifying index constraint errors with their description #712

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"prettify": "balena-lint -t tsconfig.dev.json -e js -e ts --fix src test build typings Gruntfile.ts"
},
"dependencies": {
"@balena/abstract-sql-compiler": "^9.0.4",
"@balena/abstract-sql-compiler": "^9.1.0",
"@balena/abstract-sql-to-typescript": "^2.1.3",
"@balena/env-parsing": "^1.1.10",
"@balena/lf-to-abstract-sql": "^5.0.1",
Expand Down
76 changes: 53 additions & 23 deletions src/sbvr-api/sbvr-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,34 +242,64 @@ const prettifyConstraintError = (
request: uriParser.ODataRequest,
) => {
if (err instanceof db.ConstraintError) {
let matches: RegExpExecArray | null = null;
let keyMatches: RegExpExecArray | null = null;
let violatedConstraintInfo:
| {
table: AbstractSQLCompiler.AbstractSqlTable;
name: string;
}
| undefined;
if (err instanceof db.UniqueConstraintError) {
switch (db.engine) {
case 'mysql':
matches =
keyMatches =
/ER_DUP_ENTRY: Duplicate entry '.*?[^\\]' for key '(.*?[^\\])'/.exec(
err.message,
);
break;
case 'postgres': {
const resourceName = resolveSynonym(request);
const abstractSqlModel = getFinalAbstractSqlModel(request);
matches = new RegExp(
'"' + abstractSqlModel.tables[resourceName].name + '_(.*?)_key"',
).exec(err.message);
const table = abstractSqlModel.tables[resourceName];
keyMatches = new RegExp('"' + table.name + '_(.*?)_key"').exec(
err.message,
);
// no need to run the regex if a `_key` error was already matched, or if there are
// no indexes on the table (since those are the only supported uniqueness errors atm)
if (keyMatches == null && table.indexes.length > 0) {
const indexConstraintName =
/duplicate key value violates unique constraint "(.*)"/.exec(
err.message,
)?.[1];
if (indexConstraintName != null) {
violatedConstraintInfo = {
table,
name: indexConstraintName,
};
}
}
break;
}
}
// We know it's the right error type, so if matches exists just throw a generic error message, since we have failed to get the info for a more specific one.
if (matches == null) {
throw new db.UniqueConstraintError('Unique key constraint violated');
if (keyMatches != null) {
const columns = keyMatches[1].split('_');
throw new db.UniqueConstraintError(
'"' +
columns.map(sqlNameToODataName).join('" and "') +
'" must be unique.',
);
}
const columns = matches[1].split('_');
throw new db.UniqueConstraintError(
'"' +
columns.map(sqlNameToODataName).join('" and "') +
'" must be unique.',
);
if (violatedConstraintInfo != null) {
const { table, name: violatedConstraintName } = violatedConstraintInfo;
const violatedUniqueIndex = table.indexes.find(
(idx) => idx.name === violatedConstraintName,
);
if (violatedUniqueIndex?.description != null) {
throw new BadRequestError(violatedUniqueIndex.description);
}
}
// We know it's the right error type, so if matches exists just throw a generic error message, since we have failed to get the info for a more specific one.
throw new db.UniqueConstraintError('Unique key constraint violated');
}

if (err instanceof db.ExclusionConstraintError) {
Expand All @@ -280,7 +310,7 @@ const prettifyConstraintError = (
if (err instanceof db.ForeignKeyConstraintError) {
switch (db.engine) {
case 'mysql':
matches =
keyMatches =
/ER_ROW_IS_REFERENCED_: Cannot delete or update a parent row: a foreign key constraint fails \(".*?"\.(".*?").*/.exec(
err.message,
);
Expand All @@ -289,13 +319,13 @@ const prettifyConstraintError = (
const resourceName = resolveSynonym(request);
const abstractSqlModel = getFinalAbstractSqlModel(request);
const tableName = abstractSqlModel.tables[resourceName].name;
matches = new RegExp(
keyMatches = new RegExp(
'"' +
tableName +
'" violates foreign key constraint ".*?" on table "(.*?)"',
).exec(err.message);
if (matches == null) {
matches = new RegExp(
if (keyMatches == null) {
keyMatches = new RegExp(
'"' +
tableName +
'" violates foreign key constraint "' +
Expand All @@ -308,13 +338,13 @@ const prettifyConstraintError = (
}
// We know it's the right error type, so if no matches exists just throw a generic error message,
// since we have failed to get the info for a more specific one.
if (matches == null) {
if (keyMatches == null) {
throw new db.ForeignKeyConstraintError(
'Foreign key constraint violated',
);
}
throw new db.ForeignKeyConstraintError(
'Data is referenced by ' + sqlNameToODataName(matches[1]) + '.',
'Data is referenced by ' + sqlNameToODataName(keyMatches[1]) + '.',
);
}

Expand All @@ -325,16 +355,16 @@ const prettifyConstraintError = (
if (table.checks) {
switch (db.engine) {
case 'postgres':
matches = new RegExp(
keyMatches = new RegExp(
'new row for relation "' +
table.name +
'" violates check constraint "(.*?)"',
).exec(err.message);
break;
}
}
if (matches != null) {
const checkName = matches[1];
if (keyMatches != null) {
const checkName = keyMatches[1];
const check = table.checks!.find((c) => c.name === checkName);
if (check?.description != null) {
throw new BadRequestError(check.description);
Expand Down
Loading