Add support for 'undefined column' errors and composite UNIQUE constraint violations, and clarify the documentation

master
Sven Slootweg 7 years ago
parent 7ace6ebf62
commit 082b49b932

@ -80,6 +80,18 @@ When a user attempts to register a new account, and either their provided userna
## API
A brief listing of the currently defined error types, for easier searching:
* UniqueConstraintViolationError
* ForeignKeyConstraintViolationError
* NotNullConstraintViolationError
* CheckConstraintViolationError
* InvalidTypeError
* EnumError
* UndefinedColumnError
The full documentation for each type is below.
### databaseError.rethrow(error)
Rethrows a more useful error, where possible.
@ -134,10 +146,17 @@ A `UniqueConstraintViolationError` will contain the following additional propert
* __schema:__ The schema in which the violation occurred.
* __table:__ The table in which the violation occurred.
* __column:__ The column in which the violation occurred.
* __value:__ The offending duplicate value.
* For single-column `UNIQUE` constraints:
* __column:__ The column in which the violation occurred.
* __value:__ The offending duplicate value.
* For multiple-column (composite) `UNIQUE` constraints:
* __columns:__ The columns in which the violation occurred, as an array.
* __values:__ The offending duplicate values, as an array.
* __isComposite:__ Whether the `UNIQUE` constraint involves multiple columns. Boolean.
* __constraint:__ The name of the `UNIQUE` constraint that is being violated.
Note that the keys for composite `UNIQUE` constraints are __plural__, not singular. This is intentional, to provide a more predictable API.
#### databaseError.ForeignKeyConstraintViolationError
This error is thrown when a query violates a foreign key constraint. In practice, this means that there's an attempt to point a foreign key at a non-existent entry (usually in another table).
@ -201,3 +220,13 @@ An `EnumError` will contain the following additional properties:
* __enumType:__ The name of the ENUM type for which an invalid value was inserted.
* __value:__ The value that caused the error.
* __query:__ The query that caused the error. This query may contain placeholders or be incomplete, and so is intended purely for reference purposes.
#### databaseError.UndefinedColumnError
This error is thrown when a query attempts to store a value into a column that doesn't exist. A typical cause of this is forgetting to update the database schema after updating an application.
An `UndefinedColumnError` will contain the following additional properties:
* __table:__ The table in which the value was attempted to be inserted.
* __table:__ The non-existent column in which the value was attempted to be inserted.
* __query:__ The query that caused the type error. This query may contain placeholders or be incomplete, and so is intended purely for reference purposes.

@ -0,0 +1,40 @@
'use strict';
const createError = require("create-error");
const pgErrorCodes = require("pg-error-codes");
const DatabaseError = require("../database-error.js");
const getTable = require("../get-table");
let UndefinedColumnError = createError(DatabaseError, "UndefinedColumnError");
let messageRegex = /^(.+) - column "([^"]+)" of relation "([^"]+)" does not exist$/;
module.exports = {
error: UndefinedColumnError,
errorName: "UndefinedColumnError",
check: function checkType(error) {
return (
// PostgreSQL (via `pg`):
(error.length != null && error.file != null && error.line != null && error.routine != null && error.code === "42703")
)
},
convert: function convertError(error) {
let messageMatch = messageRegex.exec(error.message);
if (messageMatch == null) {
throw new Error("Encountered unknown error format");
}
let [_, query, column, table] = messageMatch;
return new UndefinedColumnError(`The '${column}' column does not exist in the '${table}' table`, {
originalError: error,
pgCode: error.code,
code: pgErrorCodes[error.code],
query: query,
table: table,
column: column,
});
}
};

@ -3,6 +3,7 @@
const createError = require("create-error");
const pgErrorCodes = require("pg-error-codes");
const DatabaseError = require("../database-error.js");
const splitValues = require("../split-values");
let UniqueConstraintViolationError = createError(DatabaseError, "UniqueConstraintViolationError");
@ -18,16 +19,39 @@ module.exports = {
)
},
convert: function convertError(error) {
let [_, column, value] = detailsRegex.exec(error.detail);
let [_, columnValue, valueValue] = detailsRegex.exec(error.detail);
return new UniqueConstraintViolationError(`Value '${value}' already exists for column '${column}' in table '${error.table}'`, {
let column, columns, value, values, messageColumn, messageValue, isComposite;
if (columnValue.includes(",")) {
columns = splitValues(columnValue);
messageColumn = `columns ${columns.map(column => `'${column}'`).join(", ")}`;
isComposite = true;
} else {
column = columnValue;
messageColumn = `column '${column}'`;
isComposite = false;
}
if (valueValue.includes(",")) {
values = splitValues(valueValue);
messageValue = `Values ${values.map(value => `'${value}'`).join(", ")} already exist`;
} else {
value = valueValue;
messageValue = `Value '${value}' already exists`;
}
return new UniqueConstraintViolationError(`${messageValue} for ${messageColumn} in table '${error.table}'`, {
originalError: error,
pgCode: error.code,
code: pgErrorCodes[error.code],
schema: error.schema,
table: error.table,
column: column,
columns: columns,
value: value,
values: values,
isComposite: isComposite,
constraint: error.constraint
});
}

@ -12,6 +12,7 @@ let handlers = [
require("./errors/foreign-key-constraint-violation"),
require("./errors/enum"),
require("./errors/invalid-type"),
require("./errors/undefined-column"),
require("./errors/not-null-constraint-violation"),
]

@ -0,0 +1,5 @@
'use strict';
module.exports = function splitValues(values) {
return values.split(",").map(value => value.trim());
};

@ -0,0 +1,16 @@
'use strict';
module.exports = {
up: function createUniqueConstraintViolationTable(knex, errorHandler) {
return knex.schema.createTable("composite_unique_constraint_violation", (table) => {
table.increments("id");
table.text("email");
table.text("username");
table.text("name");
table.unique(["email", "username"]);
}).catch(errorHandler);
},
down: function dropUniqueConstraintViolationTable(knex, errorHandler) {
return knex.schema.dropTable("composite_unique_constraint_violation").catch(errorHandler);
}
};

@ -4,6 +4,7 @@ const Promise = require("bluebird");
let tables = [
require("./unique-constraint-violation"),
require("./composite-unique-constraint-violation"),
require("./check-constraint-violation"),
require("./foreign-key-constraint-violation"),
require("./enum"),

@ -0,0 +1,21 @@
'use strict';
module.exports = function attemptCompositeUniqueConstraintViolation(knex) {
return knex("composite_unique_constraint_violation").insert([{
email: "foo@bar.com",
username: "foo",
name: "Joe"
}, {
email: "baz@qux.com",
username: "bar",
name: "Jane"
}, {
email: "foo@bar.com",
username: "baz",
name: "Pete"
}, {
email: "foo@bar.com",
username: "foo",
name: "Jill"
}]).returning("*");
};

@ -0,0 +1,10 @@
'use strict';
module.exports = function attemptInvalidColumn(knex) {
return knex("invalid_type").insert([{
name: "Joe",
age: 29,
active: true,
nonexistentColumn: "Hello!"
}]).returning("*");
};
Loading…
Cancel
Save