diff --git a/README.md b/README.md index e4b5e9f..6a3ab8d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/errors/undefined-column.js b/src/errors/undefined-column.js new file mode 100644 index 0000000..d40a286 --- /dev/null +++ b/src/errors/undefined-column.js @@ -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, + }); + } +}; diff --git a/src/errors/unique-constraint-violation.js b/src/errors/unique-constraint-violation.js index 0c7845f..1466693 100644 --- a/src/errors/unique-constraint-violation.js +++ b/src/errors/unique-constraint-violation.js @@ -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 }); } diff --git a/src/index.js b/src/index.js index 6102480..f38b282 100644 --- a/src/index.js +++ b/src/index.js @@ -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"), ] diff --git a/src/split-values.js b/src/split-values.js new file mode 100644 index 0000000..b93ad45 --- /dev/null +++ b/src/split-values.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function splitValues(values) { + return values.split(",").map(value => value.trim()); +}; diff --git a/test/create-tables/composite-unique-constraint-violation.js b/test/create-tables/composite-unique-constraint-violation.js new file mode 100644 index 0000000..fcb06bd --- /dev/null +++ b/test/create-tables/composite-unique-constraint-violation.js @@ -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); + } +}; diff --git a/test/create-tables/index.js b/test/create-tables/index.js index 6c51f10..e109b1b 100644 --- a/test/create-tables/index.js +++ b/test/create-tables/index.js @@ -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"), diff --git a/test/testcase/composite-unique-constraint-violation.js b/test/testcase/composite-unique-constraint-violation.js new file mode 100644 index 0000000..06df69a --- /dev/null +++ b/test/testcase/composite-unique-constraint-violation.js @@ -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("*"); +}; diff --git a/test/testcase/undefined-column.js b/test/testcase/undefined-column.js new file mode 100644 index 0000000..ba480aa --- /dev/null +++ b/test/testcase/undefined-column.js @@ -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("*"); +};