@chidchanun/bcp 0.2.2 → 0.2.3
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/README.md +62 -20
- package/docs/api-manifest.json +2 -2
- package/docs/api-reference.md +21 -2
- package/docs/database-migrations.md +79 -19
- package/docs/database.md +197 -28
- package/docs/docs-web-manifest.json +4 -3
- package/docs/platform-manifest.json +14 -4
- package/docs/releases/0.2.3.md +77 -0
- package/package.json +1 -1
- package/packages/cli/src/database-migrations.ts +91 -21
- package/packages/client/src/database-mysql.ts +291 -0
- package/packages/client/src/database-postgresql.ts +355 -0
- package/packages/client/src/database-sqlite.ts +445 -0
- package/packages/client/src/database.mjs +709 -57
- package/packages/client/src/database.ts +347 -182
|
@@ -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
|
+
}
|