@chidchanun/bcp 0.2.2 → 0.2.4

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.
@@ -0,0 +1,445 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ fileURLToPath,
5
+ } from "node:url";
6
+
7
+ import type {
8
+ DatabaseAdapter,
9
+ DatabaseParameters,
10
+ ResolvedDatabaseOptions,
11
+ TransactionDatabase,
12
+ } from "./database.js";
13
+
14
+ interface SqliteRunResult {
15
+ changes: number;
16
+ lastInsertRowid: number | bigint;
17
+ }
18
+
19
+ interface SqliteStatement {
20
+ all(
21
+ ...parameters: unknown[]
22
+ ): unknown[];
23
+ run(
24
+ ...parameters: unknown[]
25
+ ): SqliteRunResult;
26
+ }
27
+
28
+ interface SqliteDatabase {
29
+ prepare(
30
+ sql: string
31
+ ): SqliteStatement;
32
+ exec(
33
+ sql: string
34
+ ): unknown;
35
+ close(): void;
36
+ }
37
+
38
+ interface SqliteDatabaseConstructor {
39
+ new (
40
+ filename: string
41
+ ): SqliteDatabase;
42
+ }
43
+
44
+ interface SqliteModule {
45
+ default?: SqliteDatabaseConstructor;
46
+ }
47
+
48
+ const SQLITE_MODULE_SPECIFIER =
49
+ "better-sqlite3";
50
+
51
+ export function createSqliteDatabaseAdapter(
52
+ options: ResolvedDatabaseOptions
53
+ ): DatabaseAdapter {
54
+ return new SqliteDatabaseAdapter(
55
+ options
56
+ );
57
+ }
58
+
59
+ class SqliteDatabaseAdapter
60
+ implements DatabaseAdapter {
61
+ readonly driver = "sqlite";
62
+
63
+ private readonly options:
64
+ ResolvedDatabaseOptions;
65
+
66
+ private databasePromise:
67
+ Promise<SqliteDatabase> | null =
68
+ null;
69
+
70
+ private operationTail:
71
+ Promise<void> =
72
+ Promise.resolve();
73
+
74
+ constructor(
75
+ options: ResolvedDatabaseOptions
76
+ ) {
77
+ this.options =
78
+ options;
79
+ }
80
+
81
+ async connect(): Promise<void> {
82
+ await this.getDatabase();
83
+ }
84
+
85
+ async query<T = unknown>(
86
+ sql: string,
87
+ parameters?: DatabaseParameters
88
+ ): Promise<T> {
89
+ return this.runExclusive(
90
+ async () => {
91
+ const database =
92
+ await this.getDatabase();
93
+
94
+ return runSqliteQuery<T>(
95
+ database,
96
+ sql,
97
+ parameters
98
+ );
99
+ }
100
+ );
101
+ }
102
+
103
+ async execute<T = unknown>(
104
+ sql: string,
105
+ parameters?: DatabaseParameters
106
+ ): Promise<T> {
107
+ return this.runExclusive(
108
+ async () => {
109
+ const database =
110
+ await this.getDatabase();
111
+
112
+ return runSqliteExecute<T>(
113
+ database,
114
+ sql,
115
+ parameters
116
+ );
117
+ }
118
+ );
119
+ }
120
+
121
+ async transaction<T>(
122
+ callback: (
123
+ database: TransactionDatabase
124
+ ) => Promise<T>
125
+ ): Promise<T> {
126
+ return this.runExclusive(
127
+ async () => {
128
+ const database =
129
+ await this.getDatabase();
130
+
131
+ database.exec(
132
+ "BEGIN"
133
+ );
134
+
135
+ const transactionDatabase:
136
+ TransactionDatabase = {
137
+ query: async <R = unknown>(
138
+ sql: string,
139
+ parameters?: DatabaseParameters
140
+ ): Promise<R> =>
141
+ runSqliteQuery<R>(
142
+ database,
143
+ sql,
144
+ parameters
145
+ ),
146
+ execute: async <R = unknown>(
147
+ sql: string,
148
+ parameters?: DatabaseParameters
149
+ ): Promise<R> =>
150
+ runSqliteExecute<R>(
151
+ database,
152
+ sql,
153
+ parameters
154
+ ),
155
+ };
156
+
157
+ try {
158
+ const result =
159
+ await callback(
160
+ transactionDatabase
161
+ );
162
+
163
+ database.exec(
164
+ "COMMIT"
165
+ );
166
+
167
+ return result;
168
+ } catch (error) {
169
+ try {
170
+ database.exec(
171
+ "ROLLBACK"
172
+ );
173
+ } catch {
174
+ // Preserve the original transaction error.
175
+ }
176
+
177
+ throw error;
178
+ }
179
+ }
180
+ );
181
+ }
182
+
183
+ async disconnect(): Promise<void> {
184
+ await this.runExclusive(
185
+ async () => {
186
+ const pendingDatabase =
187
+ this.databasePromise;
188
+
189
+ this.databasePromise =
190
+ null;
191
+
192
+ if (!pendingDatabase) {
193
+ return;
194
+ }
195
+
196
+ let database:
197
+ SqliteDatabase;
198
+
199
+ try {
200
+ database =
201
+ await pendingDatabase;
202
+ } catch {
203
+ return;
204
+ }
205
+
206
+ database.close();
207
+ }
208
+ );
209
+ }
210
+
211
+ private getDatabase():
212
+ Promise<SqliteDatabase> {
213
+ if (!this.databasePromise) {
214
+ this.databasePromise =
215
+ createSqliteDatabase(
216
+ this.options
217
+ );
218
+ }
219
+
220
+ return this.databasePromise;
221
+ }
222
+
223
+ private runExclusive<T>(
224
+ operation: () => Promise<T>
225
+ ): Promise<T> {
226
+ const result =
227
+ this.operationTail.then(
228
+ operation,
229
+ operation
230
+ );
231
+
232
+ this.operationTail =
233
+ result.then(
234
+ () => undefined,
235
+ () => undefined
236
+ );
237
+
238
+ return result;
239
+ }
240
+ }
241
+
242
+ async function createSqliteDatabase(
243
+ options: ResolvedDatabaseOptions
244
+ ): Promise<SqliteDatabase> {
245
+ let sqliteModule:
246
+ SqliteModule;
247
+
248
+ try {
249
+ sqliteModule =
250
+ await import(
251
+ SQLITE_MODULE_SPECIFIER
252
+ ) as unknown as SqliteModule;
253
+ } catch (error) {
254
+ throw new Error(
255
+ "BCP Database: SQLite requires the optional dependency better-sqlite3. Install it with `npm install better-sqlite3` or create the app with the SQLite preset.",
256
+ {
257
+ cause:
258
+ error,
259
+ }
260
+ );
261
+ }
262
+
263
+ const Database =
264
+ sqliteModule.default;
265
+
266
+ if (
267
+ typeof Database !==
268
+ "function"
269
+ ) {
270
+ throw new Error(
271
+ "BCP Database: better-sqlite3 did not expose a compatible database constructor."
272
+ );
273
+ }
274
+
275
+ const filename =
276
+ resolveSqliteFilename(
277
+ options.url ??
278
+ options.database
279
+ );
280
+
281
+ if (
282
+ filename !== ":memory:"
283
+ ) {
284
+ const resolvedFilename =
285
+ path.resolve(
286
+ filename
287
+ );
288
+
289
+ fs.mkdirSync(
290
+ path.dirname(
291
+ resolvedFilename
292
+ ),
293
+ {
294
+ recursive: true,
295
+ }
296
+ );
297
+
298
+ return new Database(
299
+ resolvedFilename
300
+ );
301
+ }
302
+
303
+ return new Database(
304
+ filename
305
+ );
306
+ }
307
+
308
+ function runSqliteQuery<T>(
309
+ database: SqliteDatabase,
310
+ sql: string,
311
+ parameters?: DatabaseParameters
312
+ ): T {
313
+ assertSql(
314
+ sql
315
+ );
316
+
317
+ const statement =
318
+ database.prepare(
319
+ sql
320
+ );
321
+
322
+ return callStatement(
323
+ statement.all.bind(
324
+ statement
325
+ ),
326
+ parameters
327
+ ) as T;
328
+ }
329
+
330
+ function runSqliteExecute<T>(
331
+ database: SqliteDatabase,
332
+ sql: string,
333
+ parameters?: DatabaseParameters
334
+ ): T {
335
+ assertSql(
336
+ sql
337
+ );
338
+
339
+ const statement =
340
+ database.prepare(
341
+ sql
342
+ );
343
+
344
+ return callStatement(
345
+ statement.run.bind(
346
+ statement
347
+ ),
348
+ parameters
349
+ ) as T;
350
+ }
351
+
352
+ function callStatement<T>(
353
+ operation: (
354
+ ...parameters: unknown[]
355
+ ) => T,
356
+ parameters?: DatabaseParameters
357
+ ): T {
358
+ if (
359
+ parameters === undefined
360
+ ) {
361
+ return operation();
362
+ }
363
+
364
+ if (
365
+ isParameterArray(
366
+ parameters
367
+ )
368
+ ) {
369
+ return operation(
370
+ ...parameters
371
+ );
372
+ }
373
+
374
+ return operation(
375
+ parameters
376
+ );
377
+ }
378
+
379
+ function isParameterArray(
380
+ value: DatabaseParameters
381
+ ): value is readonly unknown[] {
382
+ return Array.isArray(
383
+ value
384
+ );
385
+ }
386
+
387
+ function resolveSqliteFilename(
388
+ value: string
389
+ ): string {
390
+ const normalized =
391
+ value.trim();
392
+
393
+ if (
394
+ normalized === "" ||
395
+ normalized === ":memory:"
396
+ ) {
397
+ return normalized ||
398
+ ":memory:";
399
+ }
400
+
401
+ if (
402
+ normalized.startsWith(
403
+ "file:"
404
+ )
405
+ ) {
406
+ return fileURLToPath(
407
+ normalized
408
+ );
409
+ }
410
+
411
+ if (
412
+ normalized.startsWith(
413
+ "sqlite://"
414
+ )
415
+ ) {
416
+ return normalized.slice(
417
+ "sqlite://".length
418
+ );
419
+ }
420
+
421
+ if (
422
+ normalized.startsWith(
423
+ "sqlite:"
424
+ )
425
+ ) {
426
+ return normalized.slice(
427
+ "sqlite:".length
428
+ );
429
+ }
430
+
431
+ return normalized;
432
+ }
433
+
434
+ function assertSql(
435
+ sql: string
436
+ ): void {
437
+ if (
438
+ typeof sql !== "string" ||
439
+ sql.trim() === ""
440
+ ) {
441
+ throw new TypeError(
442
+ "BCP Database: SQL must be a non-empty string."
443
+ );
444
+ }
445
+ }