@platformatic/sql-mapper 3.58.1 → 3.60.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/index.d.ts +5 -1
- package/index.js +41 -0
- package/lib/entity.js +20 -2
- package/lib/errors.js +4 -0
- package/lib/queries/mysql-shared.js +6 -0
- package/lib/queries/pg.js +10 -3
- package/lib/queries/sqlite.js +6 -0
- package/package.json +3 -3
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { findNearestString } from '@platformatic/foundation'
|
|
2
2
|
import fp from 'fastify-plugin'
|
|
3
|
+
import { access, constants } from 'node:fs/promises'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
3
5
|
import { setupCache } from './lib/cache.js'
|
|
4
6
|
import { buildCleanUp } from './lib/clean-up.js'
|
|
5
7
|
import { getConnectionInfo } from './lib/connection-info.js'
|
|
6
8
|
import { buildEntity } from './lib/entity.js'
|
|
7
9
|
import {
|
|
10
|
+
CannotAccessDatabaseFileError,
|
|
8
11
|
CannotFindEntityError,
|
|
9
12
|
ConnectionStringRequiredError,
|
|
10
13
|
SpecifyProtocolError,
|
|
@@ -85,6 +88,41 @@ async function registerPostgreSQLExtensionTypes (db) {
|
|
|
85
88
|
try {
|
|
86
89
|
await db.registerTypeParser('_vector', value => db.parseArray(value, parseVector))
|
|
87
90
|
} catch {}
|
|
91
|
+
|
|
92
|
+
// Return DATE columns as 'YYYY-MM-DD' strings: parsing them into local-time
|
|
93
|
+
// JS Dates shifts the value by the process time zone offset, changing the
|
|
94
|
+
// day. 1082 is the PostgreSQL oid for the DATE type.
|
|
95
|
+
try {
|
|
96
|
+
await db.registerTypeParser(1082, value => value)
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Provide a developer friendly error instead of the bare SQLITE_CANTOPEN
|
|
101
|
+
// emitted when the database file cannot be opened or created.
|
|
102
|
+
async function checkSQLiteFileAccess (path) {
|
|
103
|
+
const file = resolve(path)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// SQLite falls back to read-only when the file exists but is not writable,
|
|
107
|
+
// so reading the file is all that is required here.
|
|
108
|
+
await access(file, constants.R_OK)
|
|
109
|
+
return
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err.code !== 'ENOENT') {
|
|
112
|
+
throw new CannotAccessDatabaseFileError(file, 'the file is not readable by the current user')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The file does not exist, SQLite will create it: the directory must exist and be writable
|
|
117
|
+
const dir = dirname(file)
|
|
118
|
+
try {
|
|
119
|
+
await access(dir, constants.W_OK | constants.X_OK)
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err.code === 'ENOENT') {
|
|
122
|
+
throw new CannotAccessDatabaseFileError(file, `the directory "${dir}" does not exist`)
|
|
123
|
+
}
|
|
124
|
+
throw new CannotAccessDatabaseFileError(file, `the directory "${dir}" is not writable by the current user`)
|
|
125
|
+
}
|
|
88
126
|
}
|
|
89
127
|
|
|
90
128
|
export async function createConnectionPool ({
|
|
@@ -138,6 +176,9 @@ export async function createConnectionPool ({
|
|
|
138
176
|
} else if (connectionString.indexOf('sqlite') === 0) {
|
|
139
177
|
const { default: sqlite } = await import('@matteo.collina/sqlite-pool')
|
|
140
178
|
const path = connectionString.replace('sqlite://', '')
|
|
179
|
+
if (connectionString !== 'sqlite://:memory:') {
|
|
180
|
+
await checkSQLiteFileAccess(path)
|
|
181
|
+
}
|
|
141
182
|
db = sqlite.default(
|
|
142
183
|
connectionString === 'sqlite://:memory:' ? undefined : path,
|
|
143
184
|
{},
|
package/lib/entity.js
CHANGED
|
@@ -103,7 +103,9 @@ function createMapper (
|
|
|
103
103
|
for (const key of Object.keys(output)) {
|
|
104
104
|
let value = output[key]
|
|
105
105
|
const newKey = fieldMapToRetrieve[key]
|
|
106
|
-
|
|
106
|
+
// Do not convert Date objects: they are serialized according to the
|
|
107
|
+
// schema of the field, like non primary key columns
|
|
108
|
+
if (primaryKeys.has(key) && value !== null && value !== undefined && !(value instanceof Date)) {
|
|
107
109
|
value = value.toString()
|
|
108
110
|
}
|
|
109
111
|
if (newKey && isVectorType(fields[key].sqlType)) {
|
|
@@ -161,6 +163,12 @@ function createMapper (
|
|
|
161
163
|
const db = getDB(args)
|
|
162
164
|
const fieldsToRetrieve = computeFields(args.fields).map(f => sql.ident(f))
|
|
163
165
|
const inputs = args.inputs
|
|
166
|
+
if (inputs === undefined || inputs === null) {
|
|
167
|
+
throw new InputNotProvidedError()
|
|
168
|
+
}
|
|
169
|
+
if (inputs.length === 0) {
|
|
170
|
+
return []
|
|
171
|
+
}
|
|
164
172
|
// This else is skipped on MySQL because of https://github.com/ForbesLindesay/atdatabases/issues/221
|
|
165
173
|
/* istanbul ignore else */
|
|
166
174
|
if (autoTimestamp) {
|
|
@@ -282,6 +290,14 @@ function createMapper (
|
|
|
282
290
|
throw new UnknownFieldError(key)
|
|
283
291
|
}
|
|
284
292
|
for (const key of Object.keys(value)) {
|
|
293
|
+
if (key === 'isNull') {
|
|
294
|
+
if (value[key] === true) {
|
|
295
|
+
criteria.push(sql`${sql.ident(field)} IS NULL`)
|
|
296
|
+
} else if (value[key] === false) {
|
|
297
|
+
criteria.push(sql`${sql.ident(field)} IS NOT NULL`)
|
|
298
|
+
}
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
285
301
|
const operator = whereMap[key]
|
|
286
302
|
/* istanbul ignore next */
|
|
287
303
|
if (!operator) {
|
|
@@ -545,7 +561,9 @@ export function buildEntity (
|
|
|
545
561
|
/* istanbul ignore next */
|
|
546
562
|
function checkSQLitePrimaryKey (constraint) {
|
|
547
563
|
if (db.isSQLite) {
|
|
548
|
-
|
|
564
|
+
// SQLite uses flexible typing: NUMBER, NUMERIC and BIGINT have the same
|
|
565
|
+
// affinity as INTEGER, while TEXT has the same affinity as VARCHAR
|
|
566
|
+
const validTypes = ['varchar', 'integer', 'uuid', 'serial', 'number', 'numeric', 'bigint', 'text']
|
|
549
567
|
const pkType = fields[constraint.column_name].sqlType.toLowerCase()
|
|
550
568
|
if (!validTypes.includes(pkType)) {
|
|
551
569
|
throw new InvalidPrimaryKeyTypeError(pkType, validTypes.join(', '))
|
package/lib/errors.js
CHANGED
|
@@ -17,6 +17,10 @@ export const TableMustBeAStringError = createError(
|
|
|
17
17
|
)
|
|
18
18
|
export const UnknownFieldError = createError(`${ERROR_PREFIX}_UNKNOWN_FIELD`, 'Unknown field %s')
|
|
19
19
|
export const InputNotProvidedError = createError(`${ERROR_PREFIX}_INPUT_NOT_PROVIDED`, 'Input not provided.')
|
|
20
|
+
export const CannotAccessDatabaseFileError = createError(
|
|
21
|
+
`${ERROR_PREFIX}_CANNOT_ACCESS_DATABASE_FILE`,
|
|
22
|
+
'Cannot open SQLite database file "%s": %s'
|
|
23
|
+
)
|
|
20
24
|
export const UnsupportedWhereClauseError = createError(
|
|
21
25
|
`${ERROR_PREFIX}_UNSUPPORTED_WHERE_CLAUSE`,
|
|
22
26
|
'Unsupported where clause %s'
|
|
@@ -8,6 +8,7 @@ export async function listTables (db, sql, schemas) {
|
|
|
8
8
|
FROM information_schema.tables
|
|
9
9
|
WHERE table_schema in (${schemaList})
|
|
10
10
|
AND TABLE_TYPE = 'BASE TABLE'
|
|
11
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
11
12
|
`)
|
|
12
13
|
return res.map(r => ({ schema: r.TABLE_SCHEMA, table: r.TABLE_NAME }))
|
|
13
14
|
} else {
|
|
@@ -16,6 +17,7 @@ export async function listTables (db, sql, schemas) {
|
|
|
16
17
|
FROM information_schema.tables
|
|
17
18
|
WHERE table_schema = (SELECT DATABASE())
|
|
18
19
|
AND TABLE_TYPE = 'BASE TABLE'
|
|
20
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
19
21
|
`)
|
|
20
22
|
return res.map(r => ({ schema: r.TABLE_SCHEMA, table: r.TABLE_NAME }))
|
|
21
23
|
}
|
|
@@ -27,6 +29,7 @@ export async function listColumns (db, sql, table, schema) {
|
|
|
27
29
|
FROM information_schema.columns
|
|
28
30
|
WHERE table_name = ${table}
|
|
29
31
|
AND table_schema = ${schema}
|
|
32
|
+
ORDER BY ordinal_position
|
|
30
33
|
`
|
|
31
34
|
return db.query(query)
|
|
32
35
|
}
|
|
@@ -39,6 +42,7 @@ export async function listConstraints (db, sql, table, schema) {
|
|
|
39
42
|
USING (constraint_name, table_schema, table_name)
|
|
40
43
|
WHERE t.table_name = ${table}
|
|
41
44
|
AND t.table_schema = ${schema}
|
|
45
|
+
ORDER BY k.constraint_name, k.ordinal_position
|
|
42
46
|
`
|
|
43
47
|
return db.query(query)
|
|
44
48
|
}
|
|
@@ -115,6 +119,7 @@ export async function listViews (db, sql, schemas) {
|
|
|
115
119
|
SELECT TABLE_SCHEMA, TABLE_NAME
|
|
116
120
|
FROM information_schema.views
|
|
117
121
|
WHERE table_schema in (${schemaList})
|
|
122
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
118
123
|
`)
|
|
119
124
|
return res.map(r => ({ schema: r.TABLE_SCHEMA, table: r.TABLE_NAME, isView: true }))
|
|
120
125
|
} else {
|
|
@@ -122,6 +127,7 @@ export async function listViews (db, sql, schemas) {
|
|
|
122
127
|
SELECT TABLE_SCHEMA, TABLE_NAME
|
|
123
128
|
FROM information_schema.views
|
|
124
129
|
WHERE table_schema = (SELECT DATABASE())
|
|
130
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
125
131
|
`)
|
|
126
132
|
return res.map(r => ({ schema: r.TABLE_SCHEMA, table: r.TABLE_NAME, isView: true }))
|
|
127
133
|
}
|
package/lib/queries/pg.js
CHANGED
|
@@ -23,7 +23,8 @@ export async function listTables (db, sql, schemas) {
|
|
|
23
23
|
SELECT tablename, schemaname
|
|
24
24
|
FROM pg_catalog.pg_tables
|
|
25
25
|
WHERE
|
|
26
|
-
schemaname in (${schemaList})
|
|
26
|
+
schemaname in (${schemaList})
|
|
27
|
+
ORDER BY schemaname, tablename`)
|
|
27
28
|
return res.map(r => ({ schema: r.schemaname, table: r.tablename }))
|
|
28
29
|
}
|
|
29
30
|
const res = await db.query(sql`
|
|
@@ -31,6 +32,7 @@ export async function listTables (db, sql, schemas) {
|
|
|
31
32
|
FROM pg_catalog.pg_tables
|
|
32
33
|
WHERE
|
|
33
34
|
schemaname = current_schema()
|
|
35
|
+
ORDER BY schemaname, tablename
|
|
34
36
|
`)
|
|
35
37
|
return res.map(r => ({ schema: r.schemaname, table: r.tablename }))
|
|
36
38
|
}
|
|
@@ -56,6 +58,7 @@ export async function listColumns (db, sql, table, schema) {
|
|
|
56
58
|
AND c.table_schema = ${schema}
|
|
57
59
|
AND a.attnum > 0
|
|
58
60
|
AND NOT a.attisdropped
|
|
61
|
+
ORDER BY a.attnum
|
|
59
62
|
`)
|
|
60
63
|
|
|
61
64
|
for (const col of res) {
|
|
@@ -86,6 +89,7 @@ export async function listConstraints (db, sql, table, schema) {
|
|
|
86
89
|
ON usage.constraint_name = usage2.constraint_name
|
|
87
90
|
AND ( usage.table_name = ${table}
|
|
88
91
|
AND usage.table_schema = ${schema} )
|
|
92
|
+
ORDER BY usage.constraint_name, usage.ordinal_position, usage2.table_name, usage2.column_name
|
|
89
93
|
`
|
|
90
94
|
const constraintsList = await db.query(query)
|
|
91
95
|
return constraintsList
|
|
@@ -117,7 +121,8 @@ export async function listEnumValues (db, sql, table, schema) {
|
|
|
117
121
|
JOIN pg_type t ON e.enumtypid = t.oid
|
|
118
122
|
JOIN information_schema.columns c on c.udt_name = t.typname
|
|
119
123
|
WHERE table_name = ${table}
|
|
120
|
-
AND table_schema = ${schema}
|
|
124
|
+
AND table_schema = ${schema}
|
|
125
|
+
ORDER BY column_name, e.enumsortorder;
|
|
121
126
|
`
|
|
122
127
|
return db.query(query)
|
|
123
128
|
}
|
|
@@ -129,7 +134,8 @@ export async function listViews (db, sql, schemas) {
|
|
|
129
134
|
SELECT viewname, schemaname
|
|
130
135
|
FROM pg_catalog.pg_views
|
|
131
136
|
WHERE
|
|
132
|
-
schemaname in (${schemaList})
|
|
137
|
+
schemaname in (${schemaList})
|
|
138
|
+
ORDER BY schemaname, viewname`)
|
|
133
139
|
return res.map(r => ({ schema: r.schemaname, table: r.viewname, isView: true }))
|
|
134
140
|
}
|
|
135
141
|
const res = await db.query(sql`
|
|
@@ -137,6 +143,7 @@ export async function listViews (db, sql, schemas) {
|
|
|
137
143
|
FROM pg_catalog.pg_views
|
|
138
144
|
WHERE
|
|
139
145
|
schemaname = current_schema()
|
|
146
|
+
ORDER BY schemaname, viewname
|
|
140
147
|
`)
|
|
141
148
|
return res.map(r => ({ schema: r.schemaname, table: r.viewname, isView: true }))
|
|
142
149
|
}
|
package/lib/queries/sqlite.js
CHANGED
|
@@ -6,6 +6,9 @@ function fixValue (value) {
|
|
|
6
6
|
return value.toISOString()
|
|
7
7
|
} else if (typeof value === 'boolean') {
|
|
8
8
|
return value ? 1 : 0
|
|
9
|
+
} else if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {
|
|
10
|
+
// This is a JSON field
|
|
11
|
+
return JSON.stringify(value)
|
|
9
12
|
}
|
|
10
13
|
return value
|
|
11
14
|
}
|
|
@@ -14,6 +17,7 @@ export async function listTables (db, sql) {
|
|
|
14
17
|
const res = await db.query(sql`
|
|
15
18
|
SELECT name FROM sqlite_master
|
|
16
19
|
WHERE type='table'
|
|
20
|
+
ORDER BY name
|
|
17
21
|
`)
|
|
18
22
|
// sqlite has no schemas
|
|
19
23
|
return res.map(r => ({ schema: null, table: r.name }))
|
|
@@ -24,6 +28,7 @@ export async function listColumns (db, sql, table) {
|
|
|
24
28
|
// therefore it is changed to pragma_table_xinfo
|
|
25
29
|
const columns = await db.query(sql`
|
|
26
30
|
SELECT * FROM pragma_table_xinfo(${table})
|
|
31
|
+
ORDER BY cid
|
|
27
32
|
`)
|
|
28
33
|
for (const column of columns) {
|
|
29
34
|
column.column_name = column.name
|
|
@@ -235,6 +240,7 @@ export async function listViews (db, sql) {
|
|
|
235
240
|
const res = await db.query(sql`
|
|
236
241
|
SELECT name FROM sqlite_master
|
|
237
242
|
WHERE type='view'
|
|
243
|
+
ORDER BY name
|
|
238
244
|
`)
|
|
239
245
|
return res.map(r => ({ schema: null, table: r.name, isView: true }))
|
|
240
246
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/sql-mapper",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.60.0",
|
|
4
4
|
"description": "A data mapper utility for SQL databases",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"camelcase": "^6.3.0",
|
|
36
36
|
"fastify-plugin": "^5.0.0",
|
|
37
37
|
"inflected": "^2.1.0",
|
|
38
|
-
"@platformatic/foundation": "3.
|
|
39
|
-
"@platformatic/telemetry": "3.
|
|
38
|
+
"@platformatic/foundation": "3.60.0",
|
|
39
|
+
"@platformatic/telemetry": "3.60.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=22.19.0"
|