@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.
@@ -1,5 +1,50 @@
1
1
  import { MongoClient } from 'mongodb'
2
2
  import { DatabaseAdapter } from './DatabaseAdapter.js'
3
+ import { normalizeDatabaseError } from './DatabaseError.js'
4
+
5
+ // Same rule the SQL builder uses: plain identifiers only, with optional
6
+ // dots for nested fields. Rejects "$where", "$or", spaces, quotes, etc.
7
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/
8
+
9
+ // Same cap as the SQL builder, so a query that works on one driver
10
+ // doesn't fail on another.
11
+ const MAX_LIST_VALUES = 1000
12
+
13
+ function assertField(name) {
14
+ if (typeof name !== 'string' || !IDENTIFIER.test(name)) {
15
+ throw new Error(`Invalid column name "${String(name)}".`)
16
+ }
17
+ return name
18
+ }
19
+
20
+ // Validates the array given to IN / NOT IN, with the same rules as SQL.
21
+ function assertList(values, operator) {
22
+ if (!Array.isArray(values)) {
23
+ throw new Error(`Operator "${operator}" needs an array of values.`)
24
+ }
25
+ if (values.length > MAX_LIST_VALUES) {
26
+ throw new Error(
27
+ `Operator "${operator}" accepts at most ${MAX_LIST_VALUES} values, got ${values.length}.`,
28
+ )
29
+ }
30
+ if (values.some((v) => v === null || v === undefined)) {
31
+ throw new Error(
32
+ `"${operator}" lists cannot contain null or undefined. Use whereNull() / whereNotNull() for null checks.`,
33
+ )
34
+ }
35
+ return values
36
+ }
37
+
38
+ // SQL LIKE -> anchored, case-insensitive regex ("%" = any run, "_" = any
39
+ // single character). Case-insensitive to match MySQL and SQLite.
40
+ function likeToRegex(pattern) {
41
+ if (typeof pattern !== 'string') {
42
+ throw new Error('LIKE needs a string pattern.')
43
+ }
44
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
45
+ const source = escaped.replace(/%/g, '.*').replace(/_/g, '.')
46
+ return new RegExp(`^${source}$`, 'is')
47
+ }
3
48
 
4
49
  export function parseQuery(sql) {
5
50
  const trimmed = sql.trim().replace(/\s+/g, ' ')
@@ -26,10 +71,6 @@ export function parseQuery(sql) {
26
71
  return { type: 'findOne', table: match[1], field: match[2] }
27
72
  }
28
73
 
29
- if ((match = /^SELECT \* FROM (\w+) WHERE (\w+) = \?$/i.exec(trimmed))) {
30
- return { type: 'findOne', table: match[1], field: match[2] }
31
- }
32
-
33
74
  if ((match = /^SELECT \* FROM (\w+)$/i.exec(trimmed))) {
34
75
  return { type: 'findAll', table: match[1] }
35
76
  }
@@ -71,9 +112,22 @@ export class MongoAdapter extends DatabaseAdapter {
71
112
  }
72
113
 
73
114
  async connect() {
74
- this.client = new MongoClient(this.url)
75
- await this.client.connect()
76
- this.db = this.client.db(this.dbName)
115
+ try {
116
+ // Fail after 5s instead of the driver's default 30s, so a stopped
117
+ // MongoDB server shows up quickly.
118
+ this.client = new MongoClient(this.url, { serverSelectionTimeoutMS: 5000 })
119
+ await this.client.connect()
120
+ this.db = this.client.db(this.dbName)
121
+ } catch (err) {
122
+ const error = normalizeDatabaseError('mongodb', err)
123
+ // Never print credentials: mongodb://user:pass@host -> mongodb://***@host
124
+ const safeUrl = String(this.url).replace(/\/\/[^@/]*@/, '//***@')
125
+ error.detail = `Tried ${safeUrl}, database "${this.dbName}"`
126
+ await this.client?.close().catch(() => {})
127
+ this.client = null
128
+ this.db = null
129
+ throw error
130
+ }
77
131
  return this
78
132
  }
79
133
 
@@ -95,6 +149,18 @@ export class MongoAdapter extends DatabaseAdapter {
95
149
  }
96
150
  }
97
151
 
152
+ // Wraps any Mongo operation so failures become DatabaseErrors.
153
+ // `sql` is whatever text best describes what was attempted: the
154
+ // SQL-like string for run/get/all, or an equivalent db.x.find(...)
155
+ // description for query/count/paginate.
156
+ async guard(ctx, fn) {
157
+ try {
158
+ return await fn()
159
+ } catch (err) {
160
+ throw normalizeDatabaseError('mongodb', err, ctx)
161
+ }
162
+ }
163
+
98
164
  async nextId(table) {
99
165
  const counters = this.db.collection('__tylix_counters')
100
166
  const result = await counters.findOneAndUpdate(
@@ -107,6 +173,10 @@ export class MongoAdapter extends DatabaseAdapter {
107
173
 
108
174
  async run(sql, params = []) {
109
175
  this.ensureConnected()
176
+ return this.guard({ sql, params }, () => this._run(sql, params))
177
+ }
178
+
179
+ async _run(sql, params) {
110
180
  const query = parseQuery(sql)
111
181
 
112
182
  if (query.type === 'createTable') {
@@ -179,100 +249,145 @@ export class MongoAdapter extends DatabaseAdapter {
179
249
 
180
250
  async get(sql, params = []) {
181
251
  this.ensureConnected()
182
- const query = parseQuery(sql)
183
-
184
- if (query.type === 'findOne') {
185
- const doc = await this.db
186
- .collection(query.table)
187
- .findOne({ [query.field]: params[0] }, { projection: { _id: 0 } })
188
- return doc ?? null
189
- }
252
+ return this.guard({ sql, params }, async () => {
253
+ const query = parseQuery(sql)
254
+
255
+ if (query.type === 'findOne') {
256
+ const doc = await this.db
257
+ .collection(query.table)
258
+ .findOne({ [query.field]: params[0] }, { projection: { _id: 0 } })
259
+ return doc ?? null
260
+ }
190
261
 
191
- throw new Error(`get() cannot handle query type "${query.type}"`)
262
+ throw new Error(`get() cannot handle query type "${query.type}"`)
263
+ })
192
264
  }
193
265
 
194
266
  async all(sql, params = []) {
195
267
  this.ensureConnected()
196
- const query = parseQuery(sql)
197
-
198
- if (query.type === 'findAll') {
199
- return this.db
200
- .collection(query.table)
201
- .find({}, { projection: { _id: 0 } })
202
- .toArray()
203
- }
268
+ return this.guard({ sql, params }, async () => {
269
+ const query = parseQuery(sql)
270
+
271
+ if (query.type === 'findAll') {
272
+ return this.db
273
+ .collection(query.table)
274
+ .find({}, { projection: { _id: 0 } })
275
+ .toArray()
276
+ }
204
277
 
205
- if (query.type === 'findOne') {
206
- return this.db
207
- .collection(query.table)
208
- .find({ [query.field]: params[0] }, { projection: { _id: 0 } })
209
- .toArray()
210
- }
278
+ if (query.type === 'findOne') {
279
+ return this.db
280
+ .collection(query.table)
281
+ .find({ [query.field]: params[0] }, { projection: { _id: 0 } })
282
+ .toArray()
283
+ }
211
284
 
212
- throw new Error(`all() cannot handle query type "${query.type}"`)
285
+ throw new Error(`all() cannot handle query type "${query.type}"`)
286
+ })
213
287
  }
214
288
 
215
289
  async count(table) {
216
290
  this.ensureConnected()
217
- return this.db.collection(table).countDocuments()
291
+ return this.guard({ sql: `db.${table}.countDocuments()` }, () =>
292
+ this.db.collection(table).countDocuments(),
293
+ )
218
294
  }
219
295
 
220
296
  async paginate(table, limit, offset) {
221
297
  this.ensureConnected()
222
- return this.db
223
- .collection(table)
224
- .find({}, { projection: { _id: 0 } })
225
- .sort({ id: 1 })
226
- .skip(offset)
227
- .limit(limit)
228
- .toArray()
298
+ return this.guard(
299
+ {
300
+ sql: `db.${table}.find({}).sort({"id":1}).skip(${offset}).limit(${limit})`,
301
+ },
302
+ () =>
303
+ this.db
304
+ .collection(table)
305
+ .find({}, { projection: { _id: 0 } })
306
+ .sort({ id: 1 })
307
+ .skip(offset)
308
+ .limit(limit)
309
+ .toArray(),
310
+ )
229
311
  }
230
312
 
231
313
  async listTables() {
232
314
  this.ensureConnected()
233
- const collections = await this.db.listCollections().toArray()
234
- return collections.map((c) => c.name)
315
+ return this.guard({ sql: 'db.listCollections()' }, async () => {
316
+ const collections = await this.db.listCollections().toArray()
317
+ return collections.map((c) => c.name)
318
+ })
235
319
  }
236
320
 
237
- columnType() {
238
- return null
239
- }
240
- }
321
+ async query(table, { wheres = [], orders = [], limitValue, offsetValue } = {}) {
322
+ this.ensureConnected()
241
323
 
242
- MongoAdapter.prototype.query = async function (
243
- table,
244
- { wheres = [], orders = [], limitValue, offsetValue } = {},
245
- ) {
246
- this.ensureConnected()
247
-
248
- const filter = {}
249
- for (const w of wheres) {
250
- if (w.operator === '=') filter[w.field] = w.value
251
- else if (w.operator === '!=') filter[w.field] = { $ne: w.value }
252
- else if (w.operator === '>') filter[w.field] = { $gt: w.value }
253
- else if (w.operator === '<') filter[w.field] = { $lt: w.value }
254
- else if (w.operator === '>=') filter[w.field] = { $gte: w.value }
255
- else if (w.operator === '<=') filter[w.field] = { $lte: w.value }
256
- else
257
- throw new Error(
258
- `Unsupported where operator "${w.operator}" for MongoAdapter`,
259
- )
260
- }
324
+ let filter = {}
325
+ const sort = {}
326
+ const description = () => {
327
+ const show = (value) =>
328
+ JSON.stringify(value, (key, v) => (v instanceof RegExp ? String(v) : v))
329
+ let text = `db.${table}.find(${show(filter)})`
330
+ if (Object.keys(sort).length) text += `.sort(${show(sort)})`
331
+ if (offsetValue !== undefined) text += `.skip(${offsetValue})`
332
+ if (limitValue !== undefined) text += `.limit(${limitValue})`
333
+ return text
334
+ }
261
335
 
262
- let cursor = this.db
263
- .collection(table)
264
- .find(filter, { projection: { _id: 0 } })
336
+ return this.guard({ get sql() { return description() } }, async () => {
337
+ // Every condition goes into $and, so several conditions on the same
338
+ // field (age > 18 AND age < 65) all apply, like SQL. Values are
339
+ // always wrapped in an operator, so an object from the client can
340
+ // never be interpreted as a query operator itself.
341
+ const conditions = wheres.map((w) => {
342
+ const field = assertField(w.field)
343
+ const operator = String(w.operator).trim().replace(/\s+/g, ' ').toUpperCase()
344
+
345
+ if (operator === '=') return { [field]: { $eq: w.value } }
346
+ if (operator === '!=' || operator === '<>')
347
+ return { [field]: { $ne: w.value } }
348
+ if (operator === '>') return { [field]: { $gt: w.value } }
349
+ if (operator === '<') return { [field]: { $lt: w.value } }
350
+ if (operator === '>=') return { [field]: { $gte: w.value } }
351
+ if (operator === '<=') return { [field]: { $lte: w.value } }
352
+ if (operator === 'LIKE')
353
+ return { [field]: { $regex: likeToRegex(w.value) } }
354
+
355
+ // Mongo treats a missing field as null, so IS NULL also matches
356
+ // documents that don't have the field at all.
357
+ if (operator === 'IS NULL') return { [field]: { $eq: null } }
358
+ if (operator === 'IS NOT NULL') return { [field]: { $ne: null } }
359
+ if (operator === 'IN')
360
+ return { [field]: { $in: assertList(w.value, operator) } }
361
+ if (operator === 'NOT IN')
362
+ return { [field]: { $nin: assertList(w.value, operator) } }
363
+
364
+ throw new Error(
365
+ `Unsupported where operator "${w.operator}" for MongoAdapter`,
366
+ )
367
+ })
265
368
 
266
- if (orders.length > 0) {
267
- const sort = {}
268
- orders.forEach((o) => {
269
- sort[o.field] = o.direction.toUpperCase() === 'DESC' ? -1 : 1
369
+ if (conditions.length > 0) filter = { $and: conditions }
370
+
371
+ let cursor = this.db
372
+ .collection(table)
373
+ .find(filter, { projection: { _id: 0 } })
374
+
375
+ if (orders.length > 0) {
376
+ orders.forEach((o) => {
377
+ sort[assertField(o.field)] =
378
+ o.direction.toUpperCase() === 'DESC' ? -1 : 1
379
+ })
380
+ cursor = cursor.sort(sort)
381
+ }
382
+
383
+ if (offsetValue !== undefined) cursor = cursor.skip(offsetValue)
384
+ if (limitValue !== undefined) cursor = cursor.limit(limitValue)
385
+
386
+ return cursor.toArray()
270
387
  })
271
- cursor = cursor.sort(sort)
272
388
  }
273
389
 
274
- if (offsetValue !== undefined) cursor = cursor.skip(offsetValue)
275
- if (limitValue !== undefined) cursor = cursor.limit(limitValue)
276
-
277
- return cursor.toArray()
278
- }
390
+ columnType() {
391
+ return null
392
+ }
393
+ }
@@ -1,5 +1,6 @@
1
1
  import mysql from 'mysql2/promise'
2
2
  import { DatabaseAdapter } from './DatabaseAdapter.js'
3
+ import { normalizeDatabaseError } from './DatabaseError.js'
3
4
 
4
5
  const COLUMN_TYPES = {
5
6
  increments: 'INT AUTO_INCREMENT PRIMARY KEY',
@@ -31,19 +32,28 @@ export class MysqlAdapter extends DatabaseAdapter {
31
32
  this.connection = null
32
33
  }
33
34
 
34
- async connect() {
35
- this.connection = mysql.createPool({
36
- ...this.config,
37
- waitForConnections: true,
38
- connectionLimit: 10,
39
- queueLimit: 0,
40
- enableKeepAlive: true,
41
- keepAliveInitialDelay: 10000,
42
- })
43
- const conn = await this.connection.getConnection() // fail fast on bad credentials
44
- conn.release()
45
- return this
46
- }
35
+ async connect() {
36
+ this.connection = mysql.createPool({
37
+ ...this.config,
38
+ waitForConnections: true,
39
+ connectionLimit: 10,
40
+ queueLimit: 0,
41
+ enableKeepAlive: true,
42
+ keepAliveInitialDelay: 10000,
43
+ })
44
+ try {
45
+ const conn = await this.connection.getConnection() // fail fast on bad credentials
46
+ conn.release()
47
+ } catch (err) {
48
+ const error = normalizeDatabaseError('mysql', err)
49
+ const { host, port, user, database } = this.config
50
+ error.detail = `Tried ${host}:${port} as "${user}", database "${database}"`
51
+ await this.connection.end().catch(() => {})
52
+ this.connection = null
53
+ throw error
54
+ }
55
+ return this
56
+ }
47
57
 
48
58
  async close() {
49
59
  if (this.connection) {
@@ -62,51 +72,51 @@ export class MysqlAdapter extends DatabaseAdapter {
62
72
  }
63
73
  }
64
74
 
65
- async run(sql, params = []) {
75
+ // The one place every statement runs through, so every MySQL failure
76
+ // is translated into a DatabaseError that carries the SQL.
77
+ async execute(sql, params = []) {
66
78
  this.ensureConnected()
67
79
  const converted = params.map(toMysqlDateTime)
68
- const [result] = await this.connection.execute(sql, converted)
80
+ try {
81
+ const [result] = await this.connection.execute(sql, converted)
82
+ return result
83
+ } catch (err) {
84
+ throw normalizeDatabaseError('mysql', err, { sql, params: converted })
85
+ }
86
+ }
87
+
88
+ async run(sql, params = []) {
89
+ const result = await this.execute(sql, params)
69
90
  return { ...result, lastInsertRowid: result.insertId }
70
91
  }
71
92
 
72
93
  async get(sql, params = []) {
73
- this.ensureConnected()
74
- const converted = params.map(toMysqlDateTime)
75
- const [rows] = await this.connection.execute(sql, converted)
94
+ const rows = await this.execute(sql, params)
76
95
  return rows[0] ?? null
77
96
  }
78
97
 
79
98
  async all(sql, params = []) {
80
- this.ensureConnected()
81
- const converted = params.map(toMysqlDateTime)
82
- const [rows] = await this.connection.execute(sql, converted)
83
- return rows
99
+ return this.execute(sql, params)
84
100
  }
85
101
 
86
102
  async count(table) {
87
- this.ensureConnected()
88
- const [rows] = await this.connection.execute(
89
- `SELECT COUNT(*) as count FROM ${table}`,
90
- )
103
+ const rows = await this.execute(`SELECT COUNT(*) as count FROM ${table}`)
91
104
  return Number(rows[0].count)
92
105
  }
93
106
 
94
107
  async paginate(table, limit, offset) {
95
- this.ensureConnected()
96
108
  // mysql2 does not support ? placeholders for LIMIT/OFFSET reliably
97
109
  // across versions, so these are inlined after being forced to
98
110
  // integers here (never from raw user input) to avoid injection.
99
111
  const safeLimit = Number.isInteger(limit) ? limit : parseInt(limit, 10)
100
112
  const safeOffset = Number.isInteger(offset) ? offset : parseInt(offset, 10)
101
- const [rows] = await this.connection.execute(
113
+ return this.execute(
102
114
  `SELECT * FROM ${table} ORDER BY id ASC LIMIT ${safeLimit} OFFSET ${safeOffset}`,
103
115
  )
104
- return rows
105
116
  }
106
117
 
107
118
  async listTables() {
108
- this.ensureConnected()
109
- const [rows] = await this.connection.execute('SHOW TABLES')
119
+ const rows = await this.execute('SHOW TABLES')
110
120
  return rows.map((r) => Object.values(r)[0])
111
121
  }
112
122
 
@@ -1,5 +1,6 @@
1
1
  import pg from 'pg'
2
2
  import { DatabaseAdapter } from './DatabaseAdapter.js'
3
+ import { normalizeDatabaseError } from './DatabaseError.js'
3
4
 
4
5
  const COLUMN_TYPES = {
5
6
  increments: 'SERIAL PRIMARY KEY',
@@ -38,8 +39,24 @@ export class PostgresAdapter extends DatabaseAdapter {
38
39
 
39
40
  async connect() {
40
41
  this.pool = new pg.Pool(this.config)
42
+ try {
43
+ // pg connects lazily, so test it now. A wrong password or a
44
+ // stopped server should fail at startup, not on the first request.
45
+ const client = await this.pool.connect()
46
+ client.release()
47
+ } catch (err) {
48
+ const error = normalizeDatabaseError('postgres', err)
49
+ const { host, port, user, database } = this.config
50
+ error.detail = this.config.connectionString
51
+ ? 'Using DATABASE_URL connection string'
52
+ : `Tried ${host}:${port} as "${user}", database "${database}"`
53
+ await this.pool.end().catch(() => {})
54
+ this.pool = null
55
+ throw error
56
+ }
41
57
  return this
42
58
  }
59
+
43
60
  async close() {
44
61
  if (this.pool) {
45
62
  await this.pool.end()
@@ -57,39 +74,43 @@ export class PostgresAdapter extends DatabaseAdapter {
57
74
  }
58
75
  }
59
76
 
60
- async run(sql, params = []) {
77
+ // The one place every statement runs through. `sql` here is the final
78
+ // SQL (with $1, $2 placeholders), exactly what PostgreSQL received.
79
+ async execute(sql, params = []) {
61
80
  this.ensureConnected()
62
- const finalSql = toPositionalPlaceholders(ensureReturningId(sql))
63
- const result = await this.pool.query(finalSql, params)
81
+ try {
82
+ return await this.pool.query(sql, params)
83
+ } catch (err) {
84
+ throw normalizeDatabaseError('postgres', err, { sql, params })
85
+ }
86
+ }
87
+
88
+ async run(sql, params = []) {
89
+ const result = await this.execute(
90
+ toPositionalPlaceholders(ensureReturningId(sql)),
91
+ params,
92
+ )
64
93
  const lastInsertRowid = result.rows[0]?.id
65
94
  return { ...result, lastInsertRowid }
66
95
  }
67
96
 
68
97
  async get(sql, params = []) {
69
- this.ensureConnected()
70
- const finalSql = toPositionalPlaceholders(sql)
71
- const result = await this.pool.query(finalSql, params)
98
+ const result = await this.execute(toPositionalPlaceholders(sql), params)
72
99
  return result.rows[0] ?? null
73
100
  }
74
101
 
75
102
  async all(sql, params = []) {
76
- this.ensureConnected()
77
- const finalSql = toPositionalPlaceholders(sql)
78
- const result = await this.pool.query(finalSql, params)
103
+ const result = await this.execute(toPositionalPlaceholders(sql), params)
79
104
  return result.rows
80
105
  }
81
106
 
82
107
  async count(table) {
83
- this.ensureConnected()
84
- const result = await this.pool.query(
85
- `SELECT COUNT(*) as count FROM ${table}`,
86
- )
108
+ const result = await this.execute(`SELECT COUNT(*) as count FROM ${table}`)
87
109
  return Number(result.rows[0].count)
88
110
  }
89
111
 
90
112
  async paginate(table, limit, offset) {
91
- this.ensureConnected()
92
- const result = await this.pool.query(
113
+ const result = await this.execute(
93
114
  `SELECT * FROM ${table} ORDER BY id ASC LIMIT $1 OFFSET $2`,
94
115
  [limit, offset],
95
116
  )
@@ -97,8 +118,7 @@ export class PostgresAdapter extends DatabaseAdapter {
97
118
  }
98
119
 
99
120
  async listTables() {
100
- this.ensureConnected()
101
- const result = await this.pool.query(
121
+ const result = await this.execute(
102
122
  "SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
103
123
  )
104
124
  return result.rows.map((r) => r.tablename)
@@ -113,4 +133,4 @@ export class PostgresAdapter extends DatabaseAdapter {
113
133
  }
114
134
  return sql
115
135
  }
116
- }
136
+ }
@@ -1,4 +1,5 @@
1
1
  import { DatabaseAdapter } from './DatabaseAdapter.js'
2
+ import { normalizeDatabaseError } from './DatabaseError.js'
2
3
 
3
4
  const COLUMN_TYPES = {
4
5
  increments: 'INTEGER PRIMARY KEY AUTOINCREMENT',
@@ -20,8 +21,14 @@ export class SqliteAdapter extends DatabaseAdapter {
20
21
  }
21
22
 
22
23
  async connect() {
23
- const { DatabaseSync } = await import('node:sqlite')
24
- this.db = new DatabaseSync(this.filename)
24
+ try {
25
+ const { DatabaseSync } = await import('node:sqlite')
26
+ this.db = new DatabaseSync(this.filename)
27
+ } catch (err) {
28
+ const error = normalizeDatabaseError('sqlite', err)
29
+ error.detail = `Database file: ${this.filename}`
30
+ throw error
31
+ }
25
32
  return this
26
33
  }
27
34
 
@@ -42,45 +49,48 @@ export class SqliteAdapter extends DatabaseAdapter {
42
49
  }
43
50
  }
44
51
 
45
- async run(sql, params = []) {
52
+ // The one place every statement runs through. SQLite reports most
53
+ // mistakes (missing table or column, bad syntax) when the statement
54
+ // is prepared, so prepare() is inside the try as well.
55
+ execute(method, sql, params = []) {
46
56
  this.ensureConnected()
47
- const stmt = this.db.prepare(sql)
48
- return stmt.run(...params)
57
+ try {
58
+ return this.db.prepare(sql)[method](...params)
59
+ } catch (err) {
60
+ throw normalizeDatabaseError('sqlite', err, { sql, params })
61
+ }
62
+ }
63
+
64
+ async run(sql, params = []) {
65
+ return this.execute('run', sql, params)
49
66
  }
50
67
 
51
68
  async get(sql, params = []) {
52
- this.ensureConnected()
53
- const stmt = this.db.prepare(sql)
54
- return stmt.get(...params) ?? null
69
+ return this.execute('get', sql, params) ?? null
55
70
  }
56
71
 
57
72
  async all(sql, params = []) {
58
- this.ensureConnected()
59
- const stmt = this.db.prepare(sql)
60
- return stmt.all(...params)
73
+ return this.execute('all', sql, params)
61
74
  }
62
75
 
63
76
  async count(table) {
64
- this.ensureConnected()
65
- const row = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get()
77
+ const row = this.execute('get', `SELECT COUNT(*) as count FROM ${table}`)
66
78
  return row.count
67
79
  }
68
80
 
69
81
  async paginate(table, limit, offset) {
70
- this.ensureConnected()
71
- const stmt = this.db.prepare(
82
+ return this.execute(
83
+ 'all',
72
84
  `SELECT * FROM ${table} ORDER BY id ASC LIMIT ? OFFSET ?`,
85
+ [limit, offset],
73
86
  )
74
- return stmt.all(limit, offset)
75
87
  }
76
88
 
77
89
  async listTables() {
78
- this.ensureConnected()
79
- const rows = this.db
80
- .prepare(
81
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
82
- )
83
- .all()
90
+ const rows = this.execute(
91
+ 'all',
92
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
93
+ )
84
94
  return rows.map((r) => r.name)
85
95
  }
86
96
 
@@ -93,4 +103,4 @@ export class SqliteAdapter extends DatabaseAdapter {
93
103
  }
94
104
  return sql
95
105
  }
96
- }
106
+ }