2
ContainsNullValuesError,
3
InvalidForeignKeyError,
8
} from '@directus/errors';
9
import type { MySQLError } from './types.js';
12
UNIQUE_VIOLATION = 'ER_DUP_ENTRY',
13
NUMERIC_VALUE_OUT_OF_RANGE = 'ER_WARN_DATA_OUT_OF_RANGE',
14
ER_DATA_TOO_LONG = 'ER_DATA_TOO_LONG',
15
NOT_NULL_VIOLATION = 'ER_BAD_NULL_ERROR',
16
FOREIGN_KEY_VIOLATION = 'ER_NO_REFERENCED_ROW_2',
17
ER_INVALID_USE_OF_NULL = 'ER_INVALID_USE_OF_NULL',
18
WARN_DATA_TRUNCATED = 'WARN_DATA_TRUNCATED',
21
export function extractError(error: MySQLError): MySQLError | Error {
23
case MySQLErrorCodes.UNIQUE_VIOLATION:
24
return uniqueViolation(error);
25
case MySQLErrorCodes.NUMERIC_VALUE_OUT_OF_RANGE:
26
return numericValueOutOfRange(error);
27
case MySQLErrorCodes.ER_DATA_TOO_LONG:
28
return valueLimitViolation(error);
29
case MySQLErrorCodes.NOT_NULL_VIOLATION:
30
return notNullViolation(error);
31
case MySQLErrorCodes.FOREIGN_KEY_VIOLATION:
32
return foreignKeyViolation(error);
33
// Note: MariaDB throws data truncated for null value error
34
case MySQLErrorCodes.ER_INVALID_USE_OF_NULL:
35
case MySQLErrorCodes.WARN_DATA_TRUNCATED:
36
return containsNullValues(error);
42
function uniqueViolation(error: MySQLError) {
43
const betweenQuotes = /'([^']+)'/g;
44
const matches = error.sqlMessage.match(betweenQuotes);
46
if (!matches) return error;
49
* MySQL's error doesn't return the field name in the error. In case the field is created through
50
* Directus (/ Knex), the key name will be `<collection>_<field>_unique` in which case we can pull
51
* the field name from the key name.
52
* If the field is the primary key it instead will be `<collection>_PRIMARY` for MySQL 8+
53
* and `PRIMARY` for MySQL 5.7 and MariaDB.
56
let collection: string | null;
57
let indexName: string;
60
if (matches[1]!.includes('.')) {
61
// MySQL 8+ style error message
63
// In case of primary key matches[1] is `'<collection>.PRIMARY'`
64
// In case of other field matches[1] is `'<collection>.<collection>_<field>_unique'`
65
[collection, indexName] = matches[1]!.slice(1, -1).split('.') as [string, string];
67
// MySQL 5.7 and MariaDB style error message
69
// In case of primary key matches[1] is `'PRIMARY'`
70
// In case of other field matches[1] is `'<collection>_<field>_unique'`
71
indexName = matches[1]!.slice(1, -1);
72
collection = indexName.includes('_') ? indexName.split('_')[0]! : null;
75
if (collection !== null && indexName.startsWith(`${collection}_`) && indexName.endsWith('_unique')) {
76
field = indexName?.slice(collection.length + 1, -7);
79
return new RecordNotUniqueError({
82
primaryKey: indexName === 'PRIMARY', // propagate information about primary key violation
86
function numericValueOutOfRange(error: MySQLError) {
87
const betweenTicks = /`([^`]+)`/g;
88
const betweenQuotes = /'([^']+)'/g;
90
const tickMatches = error.sql.match(betweenTicks);
91
const quoteMatches = error.sqlMessage.match(betweenQuotes);
93
if (!tickMatches || !quoteMatches) return error;
95
const collection = tickMatches[0]?.slice(1, -1);
96
const field = quoteMatches[0]?.slice(1, -1);
98
return new ValueOutOfRangeError({
104
function valueLimitViolation(error: MySQLError) {
105
const betweenTicks = /`([^`]+)`/g;
106
const betweenQuotes = /'([^']+)'/g;
108
const tickMatches = error.sql.match(betweenTicks);
109
const quoteMatches = error.sqlMessage.match(betweenQuotes);
111
if (!tickMatches || !quoteMatches) return error;
113
const collection = tickMatches[0]?.slice(1, -1);
114
const field = quoteMatches[0]?.slice(1, -1);
116
return new ValueTooLongError({
122
function notNullViolation(error: MySQLError) {
123
const betweenTicks = /`([^`]+)`/g;
124
const betweenQuotes = /'([^']+)'/g;
126
const tickMatches = error.sql.match(betweenTicks);
127
const quoteMatches = error.sqlMessage.match(betweenQuotes);
129
if (!tickMatches || !quoteMatches) return error;
131
const collection = tickMatches[0]?.slice(1, -1);
132
const field = quoteMatches[0]?.slice(1, -1);
134
return new NotNullViolationError({
140
function foreignKeyViolation(error: MySQLError) {
141
const betweenTicks = /`([^`]+)`/g;
142
const betweenParens = /\(([^)]+)\)/g;
144
const tickMatches = error.sqlMessage.match(betweenTicks);
145
const parenMatches = error.sql.match(betweenParens);
147
if (!tickMatches || !parenMatches) return error;
149
const collection = tickMatches[1]!.slice(1, -1)!;
150
const field = tickMatches[3]!.slice(1, -1)!;
152
return new InvalidForeignKeyError({
158
function containsNullValues(error: MySQLError) {
159
const betweenTicks = /`([^`]+)`/g;
161
// Normally, we shouldn't read from the executed SQL. In this case, we're altering a single
162
// column, so we shouldn't have the problem where multiple columns are altered at the same time
163
const tickMatches = error.sql.match(betweenTicks);
165
if (!tickMatches) return error;
167
const collection = tickMatches[0]!.slice(1, -1)!;
168
const field = tickMatches[1]!.slice(1, -1)!;
170
return new ContainsNullValuesError({ collection, field });