@platformatic/sql-mapper 3.59.0 → 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 CHANGED
@@ -136,7 +136,11 @@ export interface WhereCondition {
136
136
  /**
137
137
  * Overlaps with values
138
138
  */
139
- overlaps?: any[]
139
+ overlaps?: any[],
140
+ /**
141
+ * IS NULL (true) or IS NOT NULL (false)
142
+ */
143
+ isNull?: boolean
140
144
  }
141
145
  }
142
146
 
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
- if (primaryKeys.has(key) && value !== null && value !== undefined) {
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
- const validTypes = ['varchar', 'integer', 'uuid', 'serial']
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
  }
@@ -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.59.0",
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.59.0",
39
- "@platformatic/telemetry": "3.59.0"
38
+ "@platformatic/foundation": "3.60.0",
39
+ "@platformatic/telemetry": "3.60.0"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=22.19.0"