@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.
@@ -0,0 +1,355 @@
1
+ import type {
2
+ DatabaseAdapter,
3
+ DatabaseParameters,
4
+ ResolvedDatabaseOptions,
5
+ TransactionDatabase,
6
+ } from "./database.js";
7
+
8
+ interface PostgresqlQueryResult {
9
+ rows: unknown[];
10
+ rowCount: number | null;
11
+ command?: string;
12
+ }
13
+
14
+ interface PostgresqlExecutor {
15
+ query(
16
+ sql: string,
17
+ parameters?: readonly unknown[]
18
+ ): Promise<PostgresqlQueryResult>;
19
+ }
20
+
21
+ interface PostgresqlConnection
22
+ extends PostgresqlExecutor {
23
+ release(): void;
24
+ }
25
+
26
+ interface PostgresqlPool
27
+ extends PostgresqlExecutor {
28
+ connect(): Promise<PostgresqlConnection>;
29
+ end(): Promise<void>;
30
+ }
31
+
32
+ interface PostgresqlPoolConstructor {
33
+ new (
34
+ options: Record<string, unknown>
35
+ ): PostgresqlPool;
36
+ }
37
+
38
+ interface PostgresqlModule {
39
+ Pool?: PostgresqlPoolConstructor;
40
+ default?: {
41
+ Pool?: PostgresqlPoolConstructor;
42
+ };
43
+ }
44
+
45
+ const POSTGRESQL_MODULE_SPECIFIER =
46
+ "pg";
47
+
48
+ export function createPostgresqlDatabaseAdapter(
49
+ options: ResolvedDatabaseOptions
50
+ ): DatabaseAdapter {
51
+ return new PostgresqlDatabaseAdapter(
52
+ options
53
+ );
54
+ }
55
+
56
+ class PostgresqlDatabaseAdapter
57
+ implements DatabaseAdapter {
58
+ readonly driver = "postgresql";
59
+
60
+ private readonly options:
61
+ ResolvedDatabaseOptions;
62
+
63
+ private poolPromise:
64
+ Promise<PostgresqlPool> | null =
65
+ null;
66
+
67
+ constructor(
68
+ options: ResolvedDatabaseOptions
69
+ ) {
70
+ this.options =
71
+ options;
72
+ }
73
+
74
+ async connect(): Promise<void> {
75
+ if (!this.poolPromise) {
76
+ this.poolPromise =
77
+ createPostgresqlPool(
78
+ this.options
79
+ );
80
+ }
81
+
82
+ await this.poolPromise;
83
+ }
84
+
85
+ async query<T = unknown>(
86
+ sql: string,
87
+ parameters?: DatabaseParameters
88
+ ): Promise<T> {
89
+ assertSql(
90
+ sql
91
+ );
92
+
93
+ const pool =
94
+ await this.getPool();
95
+ const result =
96
+ await pool.query(
97
+ sql,
98
+ normalizePostgresqlParameters(
99
+ parameters
100
+ )
101
+ );
102
+
103
+ return result.rows as T;
104
+ }
105
+
106
+ async execute<T = unknown>(
107
+ sql: string,
108
+ parameters?: DatabaseParameters
109
+ ): Promise<T> {
110
+ assertSql(
111
+ sql
112
+ );
113
+
114
+ const pool =
115
+ await this.getPool();
116
+ const result =
117
+ await pool.query(
118
+ sql,
119
+ normalizePostgresqlParameters(
120
+ parameters
121
+ )
122
+ );
123
+
124
+ return result as T;
125
+ }
126
+
127
+ async transaction<T>(
128
+ callback: (
129
+ database: TransactionDatabase
130
+ ) => Promise<T>
131
+ ): Promise<T> {
132
+ assertTransactionCallback(
133
+ callback
134
+ );
135
+
136
+ const pool =
137
+ await this.getPool();
138
+ const connection =
139
+ await pool.connect();
140
+
141
+ await connection.query(
142
+ "BEGIN"
143
+ );
144
+
145
+ const transactionDatabase:
146
+ TransactionDatabase = {
147
+ query: async <R = unknown>(
148
+ sql: string,
149
+ parameters?: DatabaseParameters
150
+ ): Promise<R> => {
151
+ assertSql(
152
+ sql
153
+ );
154
+
155
+ const result =
156
+ await connection.query(
157
+ sql,
158
+ normalizePostgresqlParameters(
159
+ parameters
160
+ )
161
+ );
162
+
163
+ return result.rows as R;
164
+ },
165
+ execute: async <R = unknown>(
166
+ sql: string,
167
+ parameters?: DatabaseParameters
168
+ ): Promise<R> => {
169
+ assertSql(
170
+ sql
171
+ );
172
+
173
+ const result =
174
+ await connection.query(
175
+ sql,
176
+ normalizePostgresqlParameters(
177
+ parameters
178
+ )
179
+ );
180
+
181
+ return result as R;
182
+ },
183
+ };
184
+
185
+ try {
186
+ const result =
187
+ await callback(
188
+ transactionDatabase
189
+ );
190
+
191
+ await connection.query(
192
+ "COMMIT"
193
+ );
194
+
195
+ return result;
196
+ } catch (error) {
197
+ try {
198
+ await connection.query(
199
+ "ROLLBACK"
200
+ );
201
+ } catch {
202
+ // Preserve the original transaction error.
203
+ }
204
+
205
+ throw error;
206
+ } finally {
207
+ connection.release();
208
+ }
209
+ }
210
+
211
+ async disconnect(): Promise<void> {
212
+ const pendingPool =
213
+ this.poolPromise;
214
+
215
+ this.poolPromise =
216
+ null;
217
+
218
+ if (!pendingPool) {
219
+ return;
220
+ }
221
+
222
+ let pool:
223
+ PostgresqlPool;
224
+
225
+ try {
226
+ pool =
227
+ await pendingPool;
228
+ } catch {
229
+ return;
230
+ }
231
+
232
+ await pool.end();
233
+ }
234
+
235
+ private async getPool():
236
+ Promise<PostgresqlPool> {
237
+ await this.connect();
238
+
239
+ return this.poolPromise as
240
+ Promise<PostgresqlPool>;
241
+ }
242
+ }
243
+
244
+ async function createPostgresqlPool(
245
+ options: ResolvedDatabaseOptions
246
+ ): Promise<PostgresqlPool> {
247
+ let postgresqlModule:
248
+ PostgresqlModule;
249
+
250
+ try {
251
+ postgresqlModule =
252
+ await import(
253
+ POSTGRESQL_MODULE_SPECIFIER
254
+ ) as unknown as PostgresqlModule;
255
+ } catch (error) {
256
+ throw new Error(
257
+ "BCP Database: PostgreSQL requires the optional dependency pg. Install it with `npm install pg` or create the app with the PostgreSQL preset.",
258
+ {
259
+ cause:
260
+ error,
261
+ }
262
+ );
263
+ }
264
+
265
+ const Pool =
266
+ postgresqlModule.Pool ??
267
+ postgresqlModule.default?.Pool;
268
+
269
+ if (
270
+ typeof Pool !==
271
+ "function"
272
+ ) {
273
+ throw new Error(
274
+ "BCP Database: pg did not expose a compatible Pool constructor."
275
+ );
276
+ }
277
+
278
+ const poolOptions:
279
+ Record<string, unknown> =
280
+ options.url
281
+ ? {
282
+ connectionString:
283
+ options.url,
284
+ max:
285
+ options.connectionLimit,
286
+ }
287
+ : {
288
+ host:
289
+ options.host,
290
+ port:
291
+ options.port,
292
+ user:
293
+ options.user,
294
+ password:
295
+ options.password,
296
+ database:
297
+ options.database,
298
+ max:
299
+ options.connectionLimit,
300
+ };
301
+
302
+ return new Pool(
303
+ poolOptions
304
+ );
305
+ }
306
+
307
+ function normalizePostgresqlParameters(
308
+ parameters: DatabaseParameters | undefined
309
+ ): readonly unknown[] | undefined {
310
+ if (
311
+ parameters === undefined
312
+ ) {
313
+ return undefined;
314
+ }
315
+
316
+ if (
317
+ Array.isArray(
318
+ parameters
319
+ )
320
+ ) {
321
+ return parameters;
322
+ }
323
+
324
+ throw new TypeError(
325
+ "BCP Database: PostgreSQL parameters must be an array. Use $1, $2, ... placeholders."
326
+ );
327
+ }
328
+
329
+ function assertSql(
330
+ sql: string
331
+ ): void {
332
+ if (
333
+ typeof sql !== "string" ||
334
+ sql.trim() === ""
335
+ ) {
336
+ throw new TypeError(
337
+ "BCP Database: SQL must be a non-empty string."
338
+ );
339
+ }
340
+ }
341
+
342
+ function assertTransactionCallback(
343
+ callback: unknown
344
+ ): asserts callback is (
345
+ database: TransactionDatabase
346
+ ) => Promise<unknown> {
347
+ if (
348
+ typeof callback !==
349
+ "function"
350
+ ) {
351
+ throw new TypeError(
352
+ "BCP Database: transaction callback must be a function."
353
+ );
354
+ }
355
+ }