@tylix/orm 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/adapters/DatabaseAdapter.d.ts +3 -0
- package/src/adapters/DatabaseAdapter.js +13 -2
- package/src/adapters/DatabaseError.d.ts +52 -0
- package/src/adapters/DatabaseError.js +336 -0
- package/src/adapters/MongoAdapter.js +192 -77
- package/src/adapters/MysqlAdapter.js +41 -31
- package/src/adapters/PostgresAdapter.js +38 -18
- package/src/adapters/SqliteAdapter.js +33 -23
- package/src/model/Model.d.ts +30 -10
- package/src/model/Model.js +142 -1
- package/src/query/QueryBuilder.d.ts +9 -0
- package/src/query/QueryBuilder.js +61 -2
- package/src/query/buildSelectSql.js +110 -14
- package/src/query/types.d.ts +40 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tylix/orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test": "node --test src/**/*.test.js"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@tylix/shared": "^0.
|
|
19
|
+
"@tylix/shared": "^0.6.0",
|
|
20
20
|
"mongodb": "^6.5.0",
|
|
21
21
|
"mysql2": "^3.23.1",
|
|
22
22
|
"pg": "^8.11.0"
|
|
@@ -5,6 +5,9 @@ export interface RunResult {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export declare class DatabaseAdapter {
|
|
8
|
+
/** SQL keyword used for LIKE conditions: "LIKE" by default, "ILIKE" on Postgres. */
|
|
9
|
+
readonly likeOperator: 'LIKE' | 'ILIKE';
|
|
10
|
+
|
|
8
11
|
connect(): Promise<this>;
|
|
9
12
|
close(): Promise<void>;
|
|
10
13
|
run(sql: string, params?: unknown[]): Promise<RunResult>;
|
|
@@ -44,6 +44,15 @@ export class DatabaseAdapter {
|
|
|
44
44
|
throw new Error('listTables() not implemented')
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The SQL keyword used for LIKE conditions. Case-insensitive on MySQL and
|
|
49
|
+
* SQLite already; PostgresAdapter overrides this with "ILIKE" so all SQL
|
|
50
|
+
* drivers match the same rows.
|
|
51
|
+
*/
|
|
52
|
+
get likeOperator() {
|
|
53
|
+
return 'LIKE'
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
57
|
* Runs a structured query descriptor ({ wheres, orders, limitValue,
|
|
49
58
|
* offsetValue }) against `table` and returns matching rows.
|
|
@@ -55,7 +64,9 @@ export class DatabaseAdapter {
|
|
|
55
64
|
* overrides this entirely since it has no SQL layer to delegate to.
|
|
56
65
|
*/
|
|
57
66
|
async query(table, descriptor = {}) {
|
|
58
|
-
const { sql, params } = buildSelectSql(table, descriptor
|
|
67
|
+
const { sql, params } = buildSelectSql(table, descriptor, {
|
|
68
|
+
likeOperator: this.likeOperator,
|
|
69
|
+
})
|
|
59
70
|
return this.all(sql, params)
|
|
60
71
|
}
|
|
61
|
-
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type DatabaseDriver = 'mysql' | 'postgres' | 'sqlite' | 'mongodb';
|
|
2
|
+
|
|
3
|
+
export type DatabaseErrorKind =
|
|
4
|
+
| 'connection_failed'
|
|
5
|
+
| 'auth_failed'
|
|
6
|
+
| 'unknown_database'
|
|
7
|
+
| 'missing_table'
|
|
8
|
+
| 'missing_column'
|
|
9
|
+
| 'duplicate_key'
|
|
10
|
+
| 'not_null'
|
|
11
|
+
| 'foreign_key'
|
|
12
|
+
| 'data_too_long'
|
|
13
|
+
| 'syntax'
|
|
14
|
+
| 'unsupported_query'
|
|
15
|
+
| 'driver_missing'
|
|
16
|
+
| 'unknown';
|
|
17
|
+
|
|
18
|
+
export interface DatabaseErrorOptions {
|
|
19
|
+
driver?: DatabaseDriver | null;
|
|
20
|
+
kind?: DatabaseErrorKind;
|
|
21
|
+
title?: string | null;
|
|
22
|
+
hint?: string | null;
|
|
23
|
+
code?: string | number | null;
|
|
24
|
+
detail?: string | null;
|
|
25
|
+
table?: string | null;
|
|
26
|
+
column?: string | null;
|
|
27
|
+
sql?: string | null;
|
|
28
|
+
params?: unknown[];
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare class DatabaseError extends Error {
|
|
33
|
+
constructor(message: string, options?: DatabaseErrorOptions);
|
|
34
|
+
driver: DatabaseDriver | null;
|
|
35
|
+
kind: DatabaseErrorKind;
|
|
36
|
+
title: string | null;
|
|
37
|
+
hint: string | null;
|
|
38
|
+
code: string | number | null;
|
|
39
|
+
detail: string | null;
|
|
40
|
+
table: string | null;
|
|
41
|
+
column: string | null;
|
|
42
|
+
sql: string | null;
|
|
43
|
+
params: unknown[];
|
|
44
|
+
cause: unknown;
|
|
45
|
+
toJSON(): Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export declare function normalizeDatabaseError(
|
|
49
|
+
driver: DatabaseDriver,
|
|
50
|
+
err: unknown,
|
|
51
|
+
context?: { sql?: string | null; params?: unknown[] },
|
|
52
|
+
): DatabaseError;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
const DRIVER_LABELS = {
|
|
2
|
+
mysql: 'MySQL',
|
|
3
|
+
postgres: 'PostgreSQL',
|
|
4
|
+
sqlite: 'SQLite',
|
|
5
|
+
mongodb: 'MongoDB',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thrown by every database adapter instead of the raw driver error.
|
|
10
|
+
* Same shape for MySQL, PostgreSQL, SQLite and MongoDB, so the server
|
|
11
|
+
* and the debug page never need to know which driver is in use.
|
|
12
|
+
*/
|
|
13
|
+
export class DatabaseError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
message,
|
|
16
|
+
{
|
|
17
|
+
driver = null,
|
|
18
|
+
kind = 'unknown',
|
|
19
|
+
title = null,
|
|
20
|
+
hint = null,
|
|
21
|
+
code = null,
|
|
22
|
+
detail = null,
|
|
23
|
+
table = null,
|
|
24
|
+
column = null,
|
|
25
|
+
sql = null,
|
|
26
|
+
params = [],
|
|
27
|
+
cause = null,
|
|
28
|
+
} = {},
|
|
29
|
+
) {
|
|
30
|
+
super(message)
|
|
31
|
+
this.name = 'DatabaseError'
|
|
32
|
+
this.driver = driver
|
|
33
|
+
this.kind = kind
|
|
34
|
+
this.title = title
|
|
35
|
+
this.hint = hint
|
|
36
|
+
this.code = code
|
|
37
|
+
this.detail = detail
|
|
38
|
+
this.table = table
|
|
39
|
+
this.column = column
|
|
40
|
+
this.sql = sql
|
|
41
|
+
this.params = params
|
|
42
|
+
this.cause = cause
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Safe-to-serialize view used by the debug response. The stack is
|
|
46
|
+
// added by the server, only when debug mode is on.
|
|
47
|
+
toJSON() {
|
|
48
|
+
return {
|
|
49
|
+
name: this.name,
|
|
50
|
+
driver: this.driver,
|
|
51
|
+
kind: this.kind,
|
|
52
|
+
title: this.title,
|
|
53
|
+
message: this.message,
|
|
54
|
+
hint: this.hint,
|
|
55
|
+
code: this.code,
|
|
56
|
+
detail: this.detail,
|
|
57
|
+
table: this.table,
|
|
58
|
+
column: this.column,
|
|
59
|
+
sql: this.sql,
|
|
60
|
+
params: (this.params ?? []).map(safeParam),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeParam(value) {
|
|
66
|
+
if (typeof value === 'bigint') return value.toString()
|
|
67
|
+
if (value instanceof Date) return value.toISOString()
|
|
68
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
|
|
69
|
+
return `<${value.length} bytes>`
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === 'string' && value.length > 200) {
|
|
72
|
+
return `${value.slice(0, 200)}… (${value.length} chars)`
|
|
73
|
+
}
|
|
74
|
+
return value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------
|
|
78
|
+
// What each kind means, in plain language, and how to fix it.
|
|
79
|
+
// ---------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const KIND_INFO = {
|
|
82
|
+
connection_failed: ({ driver }) => ({
|
|
83
|
+
title: `Cannot connect to ${DRIVER_LABELS[driver] ?? 'the database'}`,
|
|
84
|
+
hint: 'Make sure the database server is running, and that the host, port and file/URL in your .env are correct.',
|
|
85
|
+
}),
|
|
86
|
+
auth_failed: () => ({
|
|
87
|
+
title: 'Database login was rejected',
|
|
88
|
+
hint: 'The username or password is wrong. Check DATABASE_USER and DATABASE_PASSWORD in your .env.',
|
|
89
|
+
}),
|
|
90
|
+
unknown_database: () => ({
|
|
91
|
+
title: 'The database does not exist',
|
|
92
|
+
hint: 'Create the database first, or fix DATABASE_NAME in your .env.',
|
|
93
|
+
}),
|
|
94
|
+
missing_table: ({ table }) => ({
|
|
95
|
+
title: table ? `Table "${table}" does not exist` : 'Table does not exist',
|
|
96
|
+
hint: 'The migration that creates this table has not run yet. Run `npx tylix migrate`, or check the table name for typos.',
|
|
97
|
+
}),
|
|
98
|
+
missing_column: ({ column, table }) => ({
|
|
99
|
+
title: column
|
|
100
|
+
? `Column "${column}" does not exist${table ? ` on "${table}"` : ''}`
|
|
101
|
+
: 'Column does not exist',
|
|
102
|
+
hint: 'Either a migration adding this column has not run (try `npx tylix migrate`), or the column name is misspelled in your model or query.',
|
|
103
|
+
}),
|
|
104
|
+
duplicate_key: ({ detail }) => ({
|
|
105
|
+
title: 'A unique rule was violated',
|
|
106
|
+
hint: `${detail ? `${detail}. ` : ''}A row with this value already exists. Check the existing data, or validate for duplicates before inserting.`,
|
|
107
|
+
}),
|
|
108
|
+
not_null: ({ column }) => ({
|
|
109
|
+
title: column ? `"${column}" cannot be empty` : 'A required value is missing',
|
|
110
|
+
hint: 'This column is NOT NULL and no value was given. Make sure the form or payload sends it, or give the column a default in the migration.',
|
|
111
|
+
}),
|
|
112
|
+
foreign_key: () => ({
|
|
113
|
+
title: 'A relationship (foreign key) rule was violated',
|
|
114
|
+
hint: 'Either you referenced a row that does not exist, or you are deleting a row that other rows still point to.',
|
|
115
|
+
}),
|
|
116
|
+
data_too_long: ({ column }) => ({
|
|
117
|
+
title: column ? `Value is too long for "${column}"` : 'Value is too long for its column',
|
|
118
|
+
hint: 'Shorten the value, or change the column to a larger type (for example `text` instead of `string`).',
|
|
119
|
+
}),
|
|
120
|
+
syntax: () => ({
|
|
121
|
+
title: 'The SQL has a syntax error',
|
|
122
|
+
hint: 'Look at the SQL below. The database stopped at the spot it quotes in the message.',
|
|
123
|
+
}),
|
|
124
|
+
unsupported_query: () => ({
|
|
125
|
+
title: 'This adapter cannot run that query',
|
|
126
|
+
hint: 'MongoAdapter only understands a limited set of simple query shapes. Use the model query builder, or extend parseQuery().',
|
|
127
|
+
}),
|
|
128
|
+
driver_missing: () => ({
|
|
129
|
+
title: 'SQLite support is not available',
|
|
130
|
+
hint: 'SQLite uses the built-in node:sqlite module, which needs Node.js 22.5 or newer. Upgrade Node, or pick another database.',
|
|
131
|
+
}),
|
|
132
|
+
unknown: ({ driver }) => ({
|
|
133
|
+
title: `${DRIVER_LABELS[driver] ?? 'Database'} error`,
|
|
134
|
+
hint: null,
|
|
135
|
+
}),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------
|
|
139
|
+
// Per-driver classifiers. Each returns { kind, table, column, detail }.
|
|
140
|
+
// ---------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
const lastSegment = (name) => (name ? name.split('.').pop() : null)
|
|
143
|
+
|
|
144
|
+
function splitQualified(name) {
|
|
145
|
+
if (!name) return { table: null, column: null }
|
|
146
|
+
const parts = name.split('.')
|
|
147
|
+
if (parts.length < 2) return { table: null, column: parts[0] }
|
|
148
|
+
return { table: parts[parts.length - 2], column: parts[parts.length - 1] }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const MYSQL_KINDS = {
|
|
152
|
+
ER_NO_SUCH_TABLE: 'missing_table',
|
|
153
|
+
ER_BAD_TABLE_ERROR: 'missing_table',
|
|
154
|
+
ER_BAD_FIELD_ERROR: 'missing_column',
|
|
155
|
+
ER_DUP_ENTRY: 'duplicate_key',
|
|
156
|
+
ER_BAD_NULL_ERROR: 'not_null',
|
|
157
|
+
ER_NO_DEFAULT_FOR_FIELD: 'not_null',
|
|
158
|
+
ER_NO_REFERENCED_ROW_2: 'foreign_key',
|
|
159
|
+
ER_ROW_IS_REFERENCED_2: 'foreign_key',
|
|
160
|
+
ER_PARSE_ERROR: 'syntax',
|
|
161
|
+
ER_ACCESS_DENIED_ERROR: 'auth_failed',
|
|
162
|
+
ER_BAD_DB_ERROR: 'unknown_database',
|
|
163
|
+
ER_DATA_TOO_LONG: 'data_too_long',
|
|
164
|
+
ECONNREFUSED: 'connection_failed',
|
|
165
|
+
ENOTFOUND: 'connection_failed',
|
|
166
|
+
ETIMEDOUT: 'connection_failed',
|
|
167
|
+
ECONNRESET: 'connection_failed',
|
|
168
|
+
PROTOCOL_CONNECTION_LOST: 'connection_failed',
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function classifyMysql(err, message) {
|
|
172
|
+
const kind = MYSQL_KINDS[err.code] ?? 'unknown'
|
|
173
|
+
let table = null
|
|
174
|
+
let column = null
|
|
175
|
+
let detail = null
|
|
176
|
+
let m
|
|
177
|
+
|
|
178
|
+
if ((m = /Table '([^']+)' doesn't exist/.exec(message))) {
|
|
179
|
+
table = lastSegment(m[1])
|
|
180
|
+
}
|
|
181
|
+
m =
|
|
182
|
+
/Unknown column '([^']+)'/.exec(message) ||
|
|
183
|
+
/Column '([^']+)' cannot be null/.exec(message) ||
|
|
184
|
+
/Field '([^']+)' doesn't have a default value/.exec(message) ||
|
|
185
|
+
/Data too long for column '([^']+)'/.exec(message)
|
|
186
|
+
if (m) column = m[1]
|
|
187
|
+
if ((m = /Duplicate entry '(.*)' for key '(.*)'/.exec(message))) {
|
|
188
|
+
detail = `Value "${m[1]}" already exists for key ${m[2]}`
|
|
189
|
+
}
|
|
190
|
+
return { kind, table, column, detail }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const PG_KINDS = {
|
|
194
|
+
'42P01': 'missing_table',
|
|
195
|
+
'42703': 'missing_column',
|
|
196
|
+
'23505': 'duplicate_key',
|
|
197
|
+
'23502': 'not_null',
|
|
198
|
+
'23503': 'foreign_key',
|
|
199
|
+
'42601': 'syntax',
|
|
200
|
+
'22001': 'data_too_long',
|
|
201
|
+
'28P01': 'auth_failed',
|
|
202
|
+
'28000': 'auth_failed',
|
|
203
|
+
'3D000': 'unknown_database',
|
|
204
|
+
ECONNREFUSED: 'connection_failed',
|
|
205
|
+
ENOTFOUND: 'connection_failed',
|
|
206
|
+
ETIMEDOUT: 'connection_failed',
|
|
207
|
+
ECONNRESET: 'connection_failed',
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function classifyPostgres(err, message) {
|
|
211
|
+
const kind = PG_KINDS[err.code] ?? 'unknown'
|
|
212
|
+
let table = err.table ?? null
|
|
213
|
+
let column = err.column ?? null
|
|
214
|
+
let m
|
|
215
|
+
|
|
216
|
+
if (!table && (m = /relation "([^"]+)" does not exist/.exec(message))) {
|
|
217
|
+
table = lastSegment(m[1])
|
|
218
|
+
}
|
|
219
|
+
if (!table && (m = /of relation "([^"]+)"/.exec(message))) table = m[1]
|
|
220
|
+
if (!column && (m = /column "([^"]+)"/.exec(message))) column = m[1]
|
|
221
|
+
|
|
222
|
+
return { kind, table, column, detail: err.detail ?? null }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function classifySqlite(err, message) {
|
|
226
|
+
let m
|
|
227
|
+
|
|
228
|
+
if (/node:sqlite/.test(message)) {
|
|
229
|
+
return { kind: 'driver_missing', table: null, column: null, detail: null }
|
|
230
|
+
}
|
|
231
|
+
if ((m = /no such table: (\S+)/.exec(message))) {
|
|
232
|
+
return { kind: 'missing_table', table: lastSegment(m[1]), column: null, detail: null }
|
|
233
|
+
}
|
|
234
|
+
if ((m = /table (\S+) has no column named (\S+)/.exec(message))) {
|
|
235
|
+
return { kind: 'missing_column', table: m[1], column: m[2], detail: null }
|
|
236
|
+
}
|
|
237
|
+
if ((m = /no such column: (\S+)/.exec(message))) {
|
|
238
|
+
return { kind: 'missing_column', table: null, column: m[1], detail: null }
|
|
239
|
+
}
|
|
240
|
+
if ((m = /UNIQUE constraint failed: (.+)/.exec(message))) {
|
|
241
|
+
const { table, column } = splitQualified(m[1].split(',')[0].trim())
|
|
242
|
+
return { kind: 'duplicate_key', table, column, detail: `Unique rule on ${m[1]}` }
|
|
243
|
+
}
|
|
244
|
+
if ((m = /NOT NULL constraint failed: (\S+)/.exec(message))) {
|
|
245
|
+
const { table, column } = splitQualified(m[1])
|
|
246
|
+
return { kind: 'not_null', table, column, detail: null }
|
|
247
|
+
}
|
|
248
|
+
if (/FOREIGN KEY constraint failed/.test(message)) {
|
|
249
|
+
return { kind: 'foreign_key', table: null, column: null, detail: null }
|
|
250
|
+
}
|
|
251
|
+
if (/syntax error/i.test(message)) {
|
|
252
|
+
return { kind: 'syntax', table: null, column: null, detail: null }
|
|
253
|
+
}
|
|
254
|
+
if (/unable to open database file/i.test(message)) {
|
|
255
|
+
return { kind: 'connection_failed', table: null, column: null, detail: null }
|
|
256
|
+
}
|
|
257
|
+
return { kind: 'unknown', table: null, column: null, detail: null }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function classifyMongo(err, message) {
|
|
261
|
+
const none = { table: null, column: null, detail: null }
|
|
262
|
+
let m
|
|
263
|
+
|
|
264
|
+
if (err.code === 11000) {
|
|
265
|
+
const table = (m = /collection: (\S+)/.exec(message)) ? lastSegment(m[1]) : null
|
|
266
|
+
const detail = err.keyValue
|
|
267
|
+
? `Value ${JSON.stringify(err.keyValue)} already exists`
|
|
268
|
+
: null
|
|
269
|
+
return { kind: 'duplicate_key', table, column: null, detail }
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
/^Mongo(ServerSelection|Network|NetworkTimeout|Parse)Error$/.test(err.name) ||
|
|
273
|
+
err.code === 'ECONNREFUSED' ||
|
|
274
|
+
err.code === 'ENOTFOUND'
|
|
275
|
+
) {
|
|
276
|
+
return { kind: 'connection_failed', ...none }
|
|
277
|
+
}
|
|
278
|
+
if (err.code === 18 || err.codeName === 'AuthenticationFailed') {
|
|
279
|
+
return { kind: 'auth_failed', ...none }
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
/does not recognize this query shape|cannot handle query type|Unsupported where operator/.test(
|
|
283
|
+
message,
|
|
284
|
+
)
|
|
285
|
+
) {
|
|
286
|
+
return { kind: 'unsupported_query', ...none }
|
|
287
|
+
}
|
|
288
|
+
return { kind: 'unknown', ...none }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const CLASSIFIERS = {
|
|
292
|
+
mysql: classifyMysql,
|
|
293
|
+
postgres: classifyPostgres,
|
|
294
|
+
sqlite: classifySqlite,
|
|
295
|
+
mongodb: classifyMongo,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Turns any driver error into a DatabaseError. Adapters call this in
|
|
300
|
+
* their catch blocks. Already-normalized errors pass through untouched.
|
|
301
|
+
*/
|
|
302
|
+
export function normalizeDatabaseError(driver, err, { sql = null, params = [] } = {}) {
|
|
303
|
+
if (err instanceof DatabaseError) return err
|
|
304
|
+
|
|
305
|
+
// mysql2 can throw an AggregateError with an empty message when
|
|
306
|
+
// "localhost" resolves to several addresses and all refuse.
|
|
307
|
+
const message =
|
|
308
|
+
err?.sqlMessage || err?.message || err?.code || String(err) || 'Unknown database error'
|
|
309
|
+
|
|
310
|
+
const classify = CLASSIFIERS[driver]
|
|
311
|
+
const info = classify
|
|
312
|
+
? classify(err ?? {}, message)
|
|
313
|
+
: { kind: 'unknown', table: null, column: null, detail: null }
|
|
314
|
+
|
|
315
|
+
const { title, hint } = (KIND_INFO[info.kind] ?? KIND_INFO.unknown)({
|
|
316
|
+
driver,
|
|
317
|
+
...info,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const error = new DatabaseError(message, {
|
|
321
|
+
driver,
|
|
322
|
+
kind: info.kind,
|
|
323
|
+
title,
|
|
324
|
+
hint,
|
|
325
|
+
code: err?.code ?? err?.codeName ?? null,
|
|
326
|
+
detail: info.detail,
|
|
327
|
+
table: info.table,
|
|
328
|
+
column: info.column,
|
|
329
|
+
sql,
|
|
330
|
+
params,
|
|
331
|
+
cause: err,
|
|
332
|
+
})
|
|
333
|
+
// Start the stack at whoever called the adapter, not inside this factory.
|
|
334
|
+
Error.captureStackTrace?.(error, normalizeDatabaseError)
|
|
335
|
+
return error
|
|
336
|
+
}
|