@iskra-bun/db-kit 0.2.0 → 0.3.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/CHANGELOG.md +53 -0
- package/README.md +9 -3
- package/dist/index.d.ts +67 -13
- package/dist/index.js +222 -129
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
- package/src/cli.ts +11 -2
- package/src/driver.ts +132 -41
- package/src/drizzle.config.template.ts +16 -5
- package/src/errors.ts +1 -1
- package/src/index.ts +1 -1
- package/src/migrations.ts +51 -14
- package/src/secrets.ts +2 -0
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* Requiere un archivo drizzle.config.ts en el directorio actual.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { drizzleKitCommand } from './migrations';
|
|
15
|
+
|
|
14
16
|
const [command, ...rest] = process.argv.slice(2);
|
|
15
17
|
|
|
16
18
|
const validCommands = ['generate', 'migrate', 'push', 'drop'];
|
|
@@ -26,12 +28,19 @@ Comandos:
|
|
|
26
28
|
generate [nombre] Genera archivos de migración a partir del schema
|
|
27
29
|
migrate Aplica migraciones pendientes
|
|
28
30
|
push Empuja el schema directo a la DB (sin migración)
|
|
29
|
-
drop Elimina
|
|
31
|
+
drop Elimina un archivo de migracion generado (no toca la DB)
|
|
30
32
|
`);
|
|
31
33
|
process.exit(command ? 1 : 0);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
// The project's own drizzle-kit, never one bunx downloads (see drizzleKitCommand).
|
|
37
|
+
let args: string[];
|
|
38
|
+
try {
|
|
39
|
+
args = drizzleKitCommand([command, ...rest]);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error((error as Error).message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
35
44
|
|
|
36
45
|
console.log(`> ${args.join(' ')}`);
|
|
37
46
|
|
package/src/driver.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type App, type Driver,
|
|
1
|
+
import { type App, type Driver, DriverError } from '@iskra-bun/core';
|
|
2
2
|
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
3
3
|
import { drizzle as drizzleMysql, type MySql2Database } from 'drizzle-orm/mysql2';
|
|
4
4
|
import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
|
|
@@ -6,8 +6,9 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
|
|
6
6
|
import postgres from 'postgres';
|
|
7
7
|
import mysql from 'mysql2/promise';
|
|
8
8
|
import { sql } from 'drizzle-orm';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
10
|
+
import { ConnectionError, MigrationError, QueryError } from './errors';
|
|
11
|
+
import { SENSITIVE_URL_PARAM } from './secrets';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Observability callback invoked for every SQL statement Drizzle executes.
|
|
@@ -21,8 +22,7 @@ export type OnQueryHook = (query: string, params: unknown[]) => void;
|
|
|
21
22
|
* the union of the supported dialect databases for the same schema. Callers can
|
|
22
23
|
* narrow by dialect if they need dialect-specific transaction APIs.
|
|
23
24
|
*/
|
|
24
|
-
export type IskraDrizzleTx<TSchema extends Record<string, unknown> = Record<string, never>> =
|
|
25
|
-
IskraDrizzleDb<TSchema>;
|
|
25
|
+
export type IskraDrizzleTx<TSchema extends Record<string, unknown> = Record<string, never>> = IskraDrizzleDb<TSchema>;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The Drizzle database handle exposed by {@link DbDriver}, parameterized by the
|
|
@@ -39,16 +39,21 @@ export type IskraDrizzleDb<TSchema extends Record<string, unknown> = Record<stri
|
|
|
39
39
|
| LibSQLDatabase<TSchema>;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Redact username
|
|
42
|
+
* Redact the username, password and secret query parameters (`authToken`,
|
|
43
|
+
* `password`, `sslpassword`...) from a database URL so it is safe to log.
|
|
43
44
|
* Returns the scrubbed URL string, or undefined if parsing fails.
|
|
44
45
|
*
|
|
45
46
|
* e.g. postgres://user:pass@host:5432/db → postgres://***:***@host:5432/db
|
|
47
|
+
* libsql://db.turso.io?authToken=eyJ… → libsql://db.turso.io?authToken=***
|
|
46
48
|
*/
|
|
47
49
|
export function scrubUrl(url: string): string | undefined {
|
|
48
50
|
try {
|
|
49
51
|
const parsed = new URL(url);
|
|
50
52
|
if (parsed.username) parsed.username = '***';
|
|
51
53
|
if (parsed.password) parsed.password = '***';
|
|
54
|
+
for (const key of [...new Set(parsed.searchParams.keys())]) {
|
|
55
|
+
if (SENSITIVE_URL_PARAM.test(key)) parsed.searchParams.set(key, '***');
|
|
56
|
+
}
|
|
52
57
|
return parsed.toString();
|
|
53
58
|
} catch {
|
|
54
59
|
return undefined;
|
|
@@ -68,6 +73,9 @@ interface DbClient {
|
|
|
68
73
|
export class DbDriver<TSchema extends Record<string, unknown> = Record<string, never>> implements Driver {
|
|
69
74
|
name = 'db';
|
|
70
75
|
private client: DbClient | undefined;
|
|
76
|
+
/** Serializes transactions on the single bun:sqlite connection. */
|
|
77
|
+
private sqliteTxQueue: Promise<unknown> = Promise.resolve();
|
|
78
|
+
private readonly inSqliteTx = new AsyncLocalStorage<true>();
|
|
71
79
|
public db: IskraDrizzleDb<TSchema> | undefined;
|
|
72
80
|
|
|
73
81
|
private app: App | undefined;
|
|
@@ -129,12 +137,14 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
129
137
|
case 'mysql': {
|
|
130
138
|
const client = mysql.createPool(config.url);
|
|
131
139
|
this.client = client;
|
|
132
|
-
|
|
140
|
+
// Name the client type: with only TSchema given, drizzle's
|
|
141
|
+
// TClient defaults to mysql2's callback Pool, not this promise Pool.
|
|
142
|
+
this.db = drizzleMysql<TSchema, typeof client>(client, { logger });
|
|
133
143
|
break;
|
|
134
144
|
}
|
|
135
145
|
case 'sqlite': {
|
|
136
|
-
const { Database } = await import(
|
|
137
|
-
const { drizzle: drizzleSqlite } = await import(
|
|
146
|
+
const { Database } = await import('bun:sqlite');
|
|
147
|
+
const { drizzle: drizzleSqlite } = await import('drizzle-orm/bun-sqlite');
|
|
138
148
|
const client = new Database(config.url);
|
|
139
149
|
this.client = client;
|
|
140
150
|
this.db = drizzleSqlite<TSchema>(client, { logger });
|
|
@@ -154,9 +164,13 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
154
164
|
context: { driver: config.driver },
|
|
155
165
|
});
|
|
156
166
|
}
|
|
167
|
+
// postgres-js and mysql2 pools connect lazily: without a round-trip
|
|
168
|
+
// a wrong host or password only surfaced on the first query.
|
|
169
|
+
await this.roundTrip();
|
|
157
170
|
this.app!.logger.info('DB connected successfully.');
|
|
158
171
|
} catch (error) {
|
|
159
172
|
if (error instanceof DriverError) throw error;
|
|
173
|
+
await this.stop();
|
|
160
174
|
this.app!.logger.error({ error }, 'Failed to connect to DB');
|
|
161
175
|
const safeUrl = scrubUrl(config.url);
|
|
162
176
|
throw new ConnectionError('Failed to connect to DB', {
|
|
@@ -170,27 +184,52 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
/**
|
|
173
|
-
*
|
|
187
|
+
* Applies the pending migrations in `migrationsDir` (generated with
|
|
188
|
+
* `drizzle-kit generate`) over the live connection, using Drizzle's
|
|
189
|
+
* migrator for the configured dialect. Requires `start()`.
|
|
190
|
+
*
|
|
191
|
+
* It used to shell out to `drizzle-kit migrate`, which ignored both
|
|
192
|
+
* arguments and failed without a drizzle.config.ts. `schemaPath` is kept for
|
|
193
|
+
* compatibility; applying migrations does not need the schema.
|
|
174
194
|
*/
|
|
175
|
-
async runMigrations(
|
|
176
|
-
|
|
195
|
+
async runMigrations(_schemaPath?: string, migrationsDir: string = './drizzle'): Promise<void> {
|
|
196
|
+
const config = this.app?.config.db;
|
|
197
|
+
if (!config) {
|
|
177
198
|
throw new DriverError('Cannot run migrations: no DB configuration found', {
|
|
178
199
|
code: 'DRIVER_START_FAILED',
|
|
179
200
|
});
|
|
180
201
|
}
|
|
202
|
+
if (!this.db) {
|
|
203
|
+
throw new DriverError('Cannot run migrations: DB is not started', {
|
|
204
|
+
code: 'DRIVER_START_FAILED',
|
|
205
|
+
context: { driver: config.driver },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
181
208
|
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
const options = { migrationsFolder: migrationsDir };
|
|
210
|
+
const db = this.db as never;
|
|
211
|
+
try {
|
|
212
|
+
switch (config.driver) {
|
|
213
|
+
case 'postgres':
|
|
214
|
+
await (await import('drizzle-orm/postgres-js/migrator')).migrate(db, options);
|
|
215
|
+
break;
|
|
216
|
+
case 'mysql':
|
|
217
|
+
await (await import('drizzle-orm/mysql2/migrator')).migrate(db, options);
|
|
218
|
+
break;
|
|
219
|
+
case 'sqlite':
|
|
220
|
+
(await import('drizzle-orm/bun-sqlite/migrator')).migrate(db, options);
|
|
221
|
+
break;
|
|
222
|
+
case 'libsql':
|
|
223
|
+
await (await import('drizzle-orm/libsql/migrator')).migrate(db, options);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
this.app!.logger.info({ migrationsDir }, 'Migrations applied');
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new MigrationError('Failed to apply migrations', {
|
|
229
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
230
|
+
context: { driver: config.driver, migrationsDir },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
194
233
|
}
|
|
195
234
|
|
|
196
235
|
/**
|
|
@@ -198,7 +237,11 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
198
237
|
* `db.transaction`. Callers receive the transaction-scoped db handle instead
|
|
199
238
|
* of reaching into the raw `db`. The dialect union means `tx` is typed as
|
|
200
239
|
* {@link IskraDrizzleTx}; narrow by dialect if you need dialect-specific APIs.
|
|
201
|
-
* Failures are wrapped in {@link QueryError}
|
|
240
|
+
* Failures are wrapped in {@link QueryError}; the transaction is rolled back.
|
|
241
|
+
*
|
|
242
|
+
* With `sqlite`, transactions run one at a time on the single connection,
|
|
243
|
+
* and a query made outside `tx` while one is open is part of it. A nested
|
|
244
|
+
* `transaction()` call is rejected: it would wait for itself.
|
|
202
245
|
*/
|
|
203
246
|
async transaction<R>(fn: (tx: IskraDrizzleTx<TSchema>) => Promise<R>): Promise<R> {
|
|
204
247
|
if (!this.db) {
|
|
@@ -207,12 +250,15 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
207
250
|
});
|
|
208
251
|
}
|
|
209
252
|
try {
|
|
253
|
+
if (this.app?.config.db?.driver === 'sqlite') return await this.sqliteTransaction(fn);
|
|
210
254
|
// The dialect-specific `transaction` overloads do not unify across the
|
|
211
255
|
// union, so we route through the runtime method with a faithful cast
|
|
212
256
|
// of the public handle types.
|
|
213
|
-
return await (
|
|
214
|
-
|
|
215
|
-
|
|
257
|
+
return await (
|
|
258
|
+
this.db as IskraDrizzleDb<TSchema> & {
|
|
259
|
+
transaction(cb: (tx: IskraDrizzleTx<TSchema>) => Promise<R>): Promise<R>;
|
|
260
|
+
}
|
|
261
|
+
).transaction((tx) => fn(tx));
|
|
216
262
|
} catch (error) {
|
|
217
263
|
if (error instanceof QueryError) throw error;
|
|
218
264
|
throw new QueryError('Transaction failed', {
|
|
@@ -222,6 +268,39 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
222
268
|
}
|
|
223
269
|
}
|
|
224
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Drizzle's bun-sqlite `transaction()` is synchronous: it commits as soon
|
|
273
|
+
* as the callback returns its promise, so an async callback that threw
|
|
274
|
+
* afterwards never rolled back. The transaction is opened and closed here
|
|
275
|
+
* around the awaited callback instead.
|
|
276
|
+
*/
|
|
277
|
+
private async sqliteTransaction<R>(fn: (tx: IskraDrizzleTx<TSchema>) => Promise<R>): Promise<R> {
|
|
278
|
+
if (this.inSqliteTx.getStore()) {
|
|
279
|
+
throw new QueryError('Nested transaction() calls are not supported with sqlite', {
|
|
280
|
+
context: { driver: 'sqlite' },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const db = this.db as BunSQLiteDatabase<TSchema>;
|
|
284
|
+
const run = () =>
|
|
285
|
+
this.inSqliteTx.run(true, async () => {
|
|
286
|
+
// A transaction handle (tx.rollback(), tx.query.*) bound to the
|
|
287
|
+
// connection; the empty native transaction it comes from is done.
|
|
288
|
+
const tx = db.transaction((t) => t);
|
|
289
|
+
db.run(sql`begin`);
|
|
290
|
+
try {
|
|
291
|
+
const result = await fn(tx as unknown as IskraDrizzleTx<TSchema>);
|
|
292
|
+
db.run(sql`commit`);
|
|
293
|
+
return result;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
db.run(sql`rollback`);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const result = this.sqliteTxQueue.then(run, run);
|
|
300
|
+
this.sqliteTxQueue = result.catch(() => undefined);
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
225
304
|
/**
|
|
226
305
|
* Liveness probe for readiness checks (e.g. web-kit's addReadinessCheck /
|
|
227
306
|
* k8s readiness). Runs a trivial `SELECT 1` against the active dialect and
|
|
@@ -230,25 +309,30 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
230
309
|
async ping(): Promise<boolean> {
|
|
231
310
|
if (!this.db) return false;
|
|
232
311
|
try {
|
|
233
|
-
|
|
234
|
-
// libsql expose the async `.execute()`. Prefer whichever exists.
|
|
235
|
-
const handle = this.db as {
|
|
236
|
-
run?(query: unknown): unknown;
|
|
237
|
-
execute?(query: unknown): Promise<unknown>;
|
|
238
|
-
};
|
|
239
|
-
if (typeof handle.run === 'function') {
|
|
240
|
-
await handle.run(sql`SELECT 1`);
|
|
241
|
-
} else if (typeof handle.execute === 'function') {
|
|
242
|
-
await handle.execute(sql`SELECT 1`);
|
|
243
|
-
} else {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
312
|
+
await this.roundTrip();
|
|
246
313
|
return true;
|
|
247
314
|
} catch {
|
|
248
315
|
return false;
|
|
249
316
|
}
|
|
250
317
|
}
|
|
251
318
|
|
|
319
|
+
/** `SELECT 1` on the active connection; throws on failure. */
|
|
320
|
+
private async roundTrip(): Promise<void> {
|
|
321
|
+
// bun-sqlite exposes the synchronous `.run()`; postgres-js, mysql2 and
|
|
322
|
+
// libsql expose the async `.execute()`. Prefer whichever exists.
|
|
323
|
+
const handle = this.db as {
|
|
324
|
+
run?(query: unknown): unknown;
|
|
325
|
+
execute?(query: unknown): Promise<unknown>;
|
|
326
|
+
};
|
|
327
|
+
if (typeof handle.run === 'function') {
|
|
328
|
+
await handle.run(sql`SELECT 1`);
|
|
329
|
+
} else if (typeof handle.execute === 'function') {
|
|
330
|
+
await handle.execute(sql`SELECT 1`);
|
|
331
|
+
} else {
|
|
332
|
+
throw new Error('DB handle exposes neither run() nor execute()');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
252
336
|
async stop() {
|
|
253
337
|
const client = this.client;
|
|
254
338
|
try {
|
|
@@ -271,3 +355,10 @@ export class DbDriver<TSchema extends Record<string, unknown> = Record<string, n
|
|
|
271
355
|
}
|
|
272
356
|
}
|
|
273
357
|
}
|
|
358
|
+
|
|
359
|
+
// `app.context.get('db')` is the DbDriver registered on the app.
|
|
360
|
+
declare module '@iskra-bun/core' {
|
|
361
|
+
interface AppContextRegistry {
|
|
362
|
+
db: DbDriver;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { defineConfig } from 'drizzle-kit';
|
|
2
|
-
|
|
3
1
|
export interface DrizzleConfigOptions {
|
|
4
2
|
/** Dialecto: 'postgresql', 'mysql', 'sqlite' */
|
|
5
3
|
dialect: 'postgresql' | 'mysql' | 'sqlite';
|
|
@@ -11,6 +9,19 @@ export interface DrizzleConfigOptions {
|
|
|
11
9
|
migrationsDir?: string;
|
|
12
10
|
}
|
|
13
11
|
|
|
12
|
+
/**
|
|
13
|
+
* La configuración que lee drizzle-kit (el `export default` de `drizzle.config.ts`).
|
|
14
|
+
* Se declara aquí para no importar drizzle-kit: es una herramienta de desarrollo,
|
|
15
|
+
* y el entry point de db-kit (que usa Bun vía la condición `bun`) fallaba al
|
|
16
|
+
* importarse donde no estaba instalada, por ejemplo con `--production`.
|
|
17
|
+
*/
|
|
18
|
+
export interface DrizzleKitConfig {
|
|
19
|
+
dialect: DrizzleConfigOptions['dialect'];
|
|
20
|
+
schema: string;
|
|
21
|
+
out: string;
|
|
22
|
+
dbCredentials: { url: string };
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Crea una configuración de drizzle-kit reutilizable.
|
|
16
27
|
*
|
|
@@ -26,13 +37,13 @@ export interface DrizzleConfigOptions {
|
|
|
26
37
|
* });
|
|
27
38
|
* ```
|
|
28
39
|
*/
|
|
29
|
-
export function createDrizzleConfig(options: DrizzleConfigOptions) {
|
|
30
|
-
return
|
|
40
|
+
export function createDrizzleConfig(options: DrizzleConfigOptions): DrizzleKitConfig {
|
|
41
|
+
return {
|
|
31
42
|
dialect: options.dialect,
|
|
32
43
|
schema: options.schemaPath,
|
|
33
44
|
out: options.migrationsDir || './drizzle',
|
|
34
45
|
dbCredentials: {
|
|
35
46
|
url: options.dbUrl,
|
|
36
47
|
},
|
|
37
|
-
}
|
|
48
|
+
};
|
|
38
49
|
}
|
package/src/errors.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './driver';
|
|
2
2
|
export * from './errors';
|
|
3
3
|
export * from './migrations';
|
|
4
|
-
export { createDrizzleConfig, type DrizzleConfigOptions } from './drizzle.config.template';
|
|
4
|
+
export { createDrizzleConfig, type DrizzleConfigOptions, type DrizzleKitConfig } from './drizzle.config.template';
|
package/src/migrations.ts
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
1
|
import { type App } from '@iskra-bun/core';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { MigrationError } from './errors';
|
|
5
|
+
import { SENSITIVE_URL_PARAM } from './secrets';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* The drizzle-kit command to run `args` with, from the project's own install:
|
|
9
|
+
* `node_modules/.bin` in `cwd` or a parent (workspaces hoist it). drizzle-kit
|
|
10
|
+
* is a devDependency, and `bunx drizzle-kit` downloaded its latest release
|
|
11
|
+
* from npm where it was missing (a production install) and ran it with
|
|
12
|
+
* DATABASE_URL in its environment. `--no-install` keeps bunx from doing so.
|
|
13
|
+
*/
|
|
14
|
+
export function drizzleKitCommand(args: string[], cwd: string = process.cwd()): string[] {
|
|
15
|
+
for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) {
|
|
16
|
+
const bin = path.join(dir, 'node_modules', '.bin', 'drizzle-kit');
|
|
17
|
+
if (['', '.exe', '.cmd'].some((ext) => existsSync(bin + ext))) {
|
|
18
|
+
return ['bunx', '--no-install', 'drizzle-kit', ...args];
|
|
19
|
+
}
|
|
20
|
+
if (path.dirname(dir) === dir) break;
|
|
21
|
+
}
|
|
22
|
+
throw new MigrationError(
|
|
23
|
+
'drizzle-kit is not installed in this project (it is never downloaded at run time): ' +
|
|
24
|
+
'add it with `bun add -d drizzle-kit`',
|
|
25
|
+
{ context: { cwd } },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Redacta credenciales `//user:pass@host` y parámetros secretos (`authToken`,
|
|
31
|
+
* `password`...) embebidos en texto arbitrario (p. ej. el stderr de drizzle-kit,
|
|
32
|
+
* que suele imprimir la cadena de conexión completa al fallar). A diferencia de
|
|
33
|
+
* `scrubUrl`, opera sobre texto libre y no requiere que el contenido sea una
|
|
34
|
+
* URL parseable, dejando intacto el resto del diagnóstico. Una contraseña con
|
|
35
|
+
* `@` o `/` sin codificar también se redacta: se toma hasta el último `@`.
|
|
9
36
|
*
|
|
10
37
|
* e.g. "... postgres://user:pass@host:5432/db" → "... postgres://***:***@host:5432/db"
|
|
11
38
|
*/
|
|
12
39
|
export function scrubCredentials(text: string): string {
|
|
13
|
-
return text
|
|
40
|
+
return text
|
|
41
|
+
.replace(/(\/\/)(?:[^\s'"`@]*@)+/g, '$1***:***@')
|
|
42
|
+
.replace(
|
|
43
|
+
new RegExp(`([?&][^=\\s&'"\`]*(?:${SENSITIVE_URL_PARAM.source})[^=\\s&'"\`]*=)[^&\\s'"\`]*`, 'gi'),
|
|
44
|
+
'$1***',
|
|
45
|
+
);
|
|
14
46
|
}
|
|
15
47
|
|
|
16
48
|
export interface MigrationConfig {
|
|
@@ -28,7 +60,8 @@ export interface MigrationConfig {
|
|
|
28
60
|
|
|
29
61
|
/**
|
|
30
62
|
* Helper para ejecutar migraciones de Drizzle Kit.
|
|
31
|
-
* Usa
|
|
63
|
+
* Usa el drizzle-kit instalado en el proyecto (`bunx --no-install drizzle-kit`)
|
|
64
|
+
* como subproceso para generar y aplicar migraciones; nunca lo descarga.
|
|
32
65
|
*/
|
|
33
66
|
export class MigrationHelper {
|
|
34
67
|
private config: MigrationConfig;
|
|
@@ -45,7 +78,7 @@ export class MigrationHelper {
|
|
|
45
78
|
* desde la config (antes se ignoraban silenciosamente).
|
|
46
79
|
*/
|
|
47
80
|
async generate(name?: string): Promise<void> {
|
|
48
|
-
const args = ['
|
|
81
|
+
const args = ['generate'];
|
|
49
82
|
if (this.config.schemaPath) args.push('--schema', this.config.schemaPath);
|
|
50
83
|
if (this.config.migrationsDir) args.push('--out', this.config.migrationsDir);
|
|
51
84
|
if (name) args.push('--name', name);
|
|
@@ -59,7 +92,7 @@ export class MigrationHelper {
|
|
|
59
92
|
* comando, por eso únicamente reenviamos configPath cuando está presente.
|
|
60
93
|
*/
|
|
61
94
|
async migrate(): Promise<void> {
|
|
62
|
-
const args = ['
|
|
95
|
+
const args = ['migrate'];
|
|
63
96
|
if (this.config.configPath) args.push('--config', this.config.configPath);
|
|
64
97
|
await this.exec(args, 'migrate');
|
|
65
98
|
}
|
|
@@ -69,18 +102,19 @@ export class MigrationHelper {
|
|
|
69
102
|
* Útil para desarrollo rápido. `push` acepta --schema pero no --out.
|
|
70
103
|
*/
|
|
71
104
|
async push(): Promise<void> {
|
|
72
|
-
const args = ['
|
|
105
|
+
const args = ['push'];
|
|
73
106
|
if (this.config.schemaPath) args.push('--schema', this.config.schemaPath);
|
|
74
107
|
if (this.config.configPath) args.push('--config', this.config.configPath);
|
|
75
108
|
await this.exec(args, 'push');
|
|
76
109
|
}
|
|
77
110
|
|
|
78
111
|
/**
|
|
79
|
-
* Elimina
|
|
112
|
+
* Elimina un archivo de migración ya generado (`drizzle-kit drop`, interactivo).
|
|
113
|
+
* No borra tablas ni datos de la base.
|
|
80
114
|
* `drop` acepta --out (dónde viven las migraciones) pero no --schema.
|
|
81
115
|
*/
|
|
82
116
|
async drop(): Promise<void> {
|
|
83
|
-
const args = ['
|
|
117
|
+
const args = ['drop'];
|
|
84
118
|
if (this.config.migrationsDir) args.push('--out', this.config.migrationsDir);
|
|
85
119
|
if (this.config.configPath) args.push('--config', this.config.configPath);
|
|
86
120
|
await this.exec(args, 'drop');
|
|
@@ -88,15 +122,18 @@ export class MigrationHelper {
|
|
|
88
122
|
|
|
89
123
|
private async exec(args: string[], operation: string): Promise<void> {
|
|
90
124
|
const env: Record<string, string> = {
|
|
91
|
-
...process.env as Record<string, string
|
|
125
|
+
...(process.env as Record<string, string>),
|
|
92
126
|
DATABASE_URL: this.config.dbUrl,
|
|
93
127
|
};
|
|
128
|
+
const cwd = process.cwd();
|
|
129
|
+
// Before anything runs with DATABASE_URL: never a downloaded drizzle-kit.
|
|
130
|
+
const command = drizzleKitCommand(args, cwd);
|
|
94
131
|
|
|
95
132
|
this.app?.logger.info(`Running migration: ${operation}`);
|
|
96
133
|
|
|
97
134
|
try {
|
|
98
|
-
const proc = Bun.spawn(
|
|
99
|
-
cwd
|
|
135
|
+
const proc = Bun.spawn(command, {
|
|
136
|
+
cwd,
|
|
100
137
|
env,
|
|
101
138
|
stdout: 'pipe',
|
|
102
139
|
stderr: 'pipe',
|
package/src/secrets.ts
ADDED