@pikku/cli 0.12.38 → 0.12.40

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.
Files changed (81) hide show
  1. package/console-app/assets/{index-Dxl3JsMK.js → index-D9Z9rySK.js} +2 -2
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  14. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  15. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  16. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  17. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  18. package/dist/.pikku/function/pikku-functions-meta.gen.json +135 -135
  19. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  20. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  21. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  22. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  23. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  25. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  26. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  27. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  28. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  29. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  30. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  31. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-types.gen.js +1 -1
  34. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  35. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  36. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  38. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  39. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  40. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +3 -3
  41. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  42. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  43. package/dist/.pikku/schemas/register.gen.js +5 -5
  44. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  45. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  46. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  47. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  48. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  49. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  50. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  51. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  52. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  53. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  54. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  55. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  56. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  57. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  58. package/dist/bin/pikku-bin.mjs +2 -2
  59. package/dist/src/deploy/build-pipeline.d.ts +1 -0
  60. package/dist/src/deploy/build-pipeline.js +1 -1
  61. package/dist/src/fabric/functions/validate-core.js +2 -7
  62. package/dist/src/fabric/functions/validate.function.js +16 -14
  63. package/dist/src/functions/commands/db-generate.js +0 -3
  64. package/dist/src/functions/commands/db-reset.js +11 -7
  65. package/dist/src/functions/commands/db-seed.js +4 -7
  66. package/dist/src/functions/commands/db-shared.js +2 -4
  67. package/dist/src/functions/commands/deploy-apply.js +1 -0
  68. package/dist/src/functions/commands/deploy-plan.js +1 -0
  69. package/dist/src/functions/commands/dev.js +1 -1
  70. package/dist/src/functions/db/local-db.d.ts +9 -5
  71. package/dist/src/functions/db/local-db.js +287 -108
  72. package/dist/src/functions/db/postgres/pglite-kysely.d.ts +8 -0
  73. package/dist/src/functions/db/postgres/pglite-kysely.js +79 -0
  74. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +1 -0
  75. package/dist/src/functions/db/postgres/postgres-introspector.js +6 -1
  76. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +7 -2
  77. package/dist/src/functions/db/postgres/postgres-migrator.js +6 -1
  78. package/dist/src/functions/validate/workspace-validate.js +4 -4
  79. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  80. package/dist/tsconfig.tsbuildinfo +1 -1
  81. package/package.json +3 -2
@@ -3,7 +3,7 @@ import { resolve, isAbsolute, relative, dirname, join } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { runInNewContext } from 'node:vm';
5
5
  import { transformSync } from 'esbuild';
6
- import { CamelCasePlugin, CompiledQuery, Kysely, PostgresDialect } from 'kysely';
6
+ import { CamelCasePlugin, Kysely, PostgresDialect } from 'kysely';
7
7
  import { migrate } from './db-migrator.js';
8
8
  import { loadAuthOptions, getAuthMigrations } from './better-auth-schema.js';
9
9
  import { generateSchemaTypes } from './db-codegen.js';
@@ -15,6 +15,7 @@ import { createSqliteKysely } from './sqlite/sqlite-kysely.js';
15
15
  import { loadSqliteRuntime } from './sqlite/sqlite-runtime.js';
16
16
  import { seed as runSeed } from './sqlite/seed.js';
17
17
  import { PostgresMigrationExecutor } from './postgres/postgres-migrator.js';
18
+ import { createPGliteKysely } from './postgres/pglite-kysely.js';
18
19
  import { PostgresIntrospector } from './postgres/postgres-introspector.js';
19
20
  // ─── Resolution ───────────────────────────────────────────────────────────────
20
21
  /**
@@ -35,6 +36,9 @@ export function parseDatabaseUrl(url) {
35
36
  * Returns null when neither sqliteDb nor postgresUrl is configured.
36
37
  */
37
38
  export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
39
+ const resolvedRuntimeDir = runtimeDir
40
+ ? resolveAgainst(rootDir, runtimeDir)
41
+ : join(rootDir, '.pikku-runtime');
38
42
  const base = (sub) => ({
39
43
  rootDir,
40
44
  migrationsDir: resolveAgainst(rootDir, sub),
@@ -58,7 +62,10 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
58
62
  if (userConfig.postgresUrl) {
59
63
  return {
60
64
  dialect: 'postgres',
65
+ mode: 'url',
61
66
  connectionString: userConfig.postgresUrl,
67
+ runtimeDir: resolvedRuntimeDir,
68
+ seedFile: resolveAgainst(rootDir, 'db/postgres-seed.sql'),
62
69
  ...base('db/postgres'),
63
70
  };
64
71
  }
@@ -67,9 +74,6 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
67
74
  ? '.pikku-runtime/dev.db'
68
75
  : undefined);
69
76
  if (sqliteDb) {
70
- const resolvedRuntimeDir = runtimeDir
71
- ? resolveAgainst(rootDir, runtimeDir)
72
- : join(rootDir, '.pikku-runtime');
73
77
  return {
74
78
  dialect: 'sqlite',
75
79
  dbFile: resolveAgainst(rootDir, sqliteDb),
@@ -78,6 +82,16 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
78
82
  ...base('db/sqlite'),
79
83
  };
80
84
  }
85
+ if (existsSync(join(rootDir, 'db/postgres'))) {
86
+ return {
87
+ dialect: 'postgres',
88
+ mode: 'pglite',
89
+ pgliteDir: join(resolvedRuntimeDir, 'dev-postgres'),
90
+ runtimeDir: resolvedRuntimeDir,
91
+ seedFile: resolveAgainst(rootDir, 'db/postgres-seed.sql'),
92
+ ...base('db/postgres'),
93
+ };
94
+ }
81
95
  return null;
82
96
  }
83
97
  /** @deprecated Use resolveDb(userConfig, ...) instead. */
@@ -90,6 +104,64 @@ export function resolveLocalDb(sqliteDb, rootDir, outDir, runtimeDir) {
90
104
  function resolveAgainst(root, p) {
91
105
  return isAbsolute(p) ? p : resolve(root, p);
92
106
  }
107
+ async function createPostgresClient(resolved) {
108
+ if (resolved.mode === 'url') {
109
+ const { Client } = await import('pg');
110
+ const client = new Client({ connectionString: resolved.connectionString });
111
+ await client.connect();
112
+ return Object.assign(client, {
113
+ __connectionString: resolved.connectionString,
114
+ });
115
+ }
116
+ if (!resolved.pgliteDir) {
117
+ throw new Error('PGlite Postgres resolution is missing pgliteDir.');
118
+ }
119
+ mkdirSync(dirname(resolved.pgliteDir), { recursive: true });
120
+ const db = await createEmbeddedPostgres(resolved.pgliteDir);
121
+ return pgliteAsClient(db);
122
+ }
123
+ async function createEmbeddedPostgres(dataDir) {
124
+ const [{ PGlite }, { pgcrypto }] = await Promise.all([
125
+ import('@electric-sql/pglite'),
126
+ import('@electric-sql/pglite/contrib/pgcrypto'),
127
+ ]);
128
+ return new PGlite({
129
+ ...(dataDir ? { dataDir } : {}),
130
+ extensions: {
131
+ pgcrypto,
132
+ },
133
+ });
134
+ }
135
+ function pgliteAsClient(db) {
136
+ return {
137
+ query: (sql, params) => db.query(sql, params),
138
+ exec: (sql) => db.exec(sql),
139
+ __pglite: db,
140
+ end: async () => {
141
+ if (!db.closed) {
142
+ await db.close();
143
+ }
144
+ },
145
+ };
146
+ }
147
+ async function withPostgresClient(resolved, run) {
148
+ const client = await createPostgresClient(resolved);
149
+ try {
150
+ return await run(client);
151
+ }
152
+ finally {
153
+ await client.end();
154
+ }
155
+ }
156
+ async function loadCoercionPlugin(coercionFile) {
157
+ try {
158
+ const mod = await import(coercionFile);
159
+ return mod.coercionMap;
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ }
93
165
  export async function migrateAndCodegen(resolved) {
94
166
  let migrateResult;
95
167
  let codegenResult;
@@ -120,13 +192,9 @@ export async function migrateAndCodegen(resolved) {
120
192
  }
121
193
  }
122
194
  else {
123
- // Postgres
124
- const introspector = new PostgresIntrospector(resolved.connectionString);
125
- await introspector.connect();
126
- try {
127
- const { Client } = await import('pg');
128
- const client = new Client({ connectionString: resolved.connectionString });
129
- await client.connect();
195
+ await withPostgresClient(resolved, async (client) => {
196
+ const introspector = new PostgresIntrospector(client);
197
+ await introspector.connect();
130
198
  try {
131
199
  const executor = new PostgresMigrationExecutor(client);
132
200
  migrateResult = await migrate(executor, resolved.migrationsDir);
@@ -142,12 +210,9 @@ export async function migrateAndCodegen(resolved) {
142
210
  });
143
211
  }
144
212
  finally {
145
- await client.end();
213
+ await introspector.close();
146
214
  }
147
- }
148
- finally {
149
- await introspector.close();
150
- }
215
+ });
151
216
  }
152
217
  const zodResult = generateZodTypes({
153
218
  schemaFile: resolved.schemaFile,
@@ -169,26 +234,74 @@ export async function migrateAndCodegen(resolved) {
169
234
  }
170
235
  // ─── SQLite-only operations ───────────────────────────────────────────────────
171
236
  export async function seed(resolved) {
172
- const runtime = await loadSqliteRuntime();
173
- const db = runtime.open(resolved.dbFile);
174
- try {
175
- return runSeed(db, resolved.seedFile);
237
+ if (resolved.dialect === 'sqlite') {
238
+ const runtime = await loadSqliteRuntime();
239
+ const db = runtime.open(resolved.dbFile);
240
+ try {
241
+ return runSeed(db, resolved.seedFile);
242
+ }
243
+ finally {
244
+ db.close();
245
+ }
176
246
  }
177
- finally {
178
- db.close();
247
+ if (!existsSync(resolved.seedFile)) {
248
+ return { applied: false, bytes: 0 };
249
+ }
250
+ const sql = readFileSync(resolved.seedFile, 'utf8');
251
+ if (sql.trim().length === 0) {
252
+ return { applied: false, bytes: 0 };
179
253
  }
254
+ await withPostgresClient(resolved, async (client) => {
255
+ if (typeof client.exec === 'function') {
256
+ await client.exec(sql);
257
+ }
258
+ else {
259
+ await client.query(sql);
260
+ }
261
+ });
262
+ return { applied: true, bytes: Buffer.byteLength(sql) };
180
263
  }
181
- export function reset(resolved, rootDir) {
264
+ export async function reset(resolved, rootDir) {
182
265
  if (process.env.NODE_ENV === 'production') {
183
266
  throw new Error(`pikku db reset refused: NODE_ENV=production. This command only runs in dev.`);
184
267
  }
185
- const rel = relative(resolved.runtimeDir, resolved.dbFile);
186
- if (rel.startsWith('..') || isAbsolute(rel)) {
187
- throw new Error(`pikku db reset refused: resolved DB file (${resolved.dbFile}) is outside the runtime directory (${resolved.runtimeDir}). Override sqliteDb or set runtimeDir correctly.`);
188
- }
189
- if (existsSync(resolved.dbFile)) {
190
- rmSync(resolved.dbFile);
268
+ if (resolved.dialect === 'sqlite') {
269
+ const rel = relative(resolved.runtimeDir, resolved.dbFile);
270
+ if (rel.startsWith('..') || isAbsolute(rel)) {
271
+ throw new Error(`pikku db reset refused: resolved DB file (${resolved.dbFile}) is outside the runtime directory (${resolved.runtimeDir}). Override sqliteDb or set runtimeDir correctly.`);
272
+ }
273
+ if (existsSync(resolved.dbFile)) {
274
+ rmSync(resolved.dbFile);
275
+ }
276
+ return;
191
277
  }
278
+ if (resolved.mode === 'pglite') {
279
+ if (!resolved.pgliteDir) {
280
+ throw new Error('PGlite Postgres resolution is missing pgliteDir.');
281
+ }
282
+ const rel = relative(resolved.runtimeDir, resolved.pgliteDir);
283
+ if (rel.startsWith('..') || isAbsolute(rel)) {
284
+ throw new Error(`pikku db reset refused: resolved PGlite dir (${resolved.pgliteDir}) is outside the runtime directory (${resolved.runtimeDir}).`);
285
+ }
286
+ if (existsSync(resolved.pgliteDir)) {
287
+ rmSync(resolved.pgliteDir, { recursive: true, force: true });
288
+ }
289
+ return;
290
+ }
291
+ await withPostgresClient(resolved, async (client) => {
292
+ const result = await client.query(`
293
+ SELECT schema_name
294
+ FROM information_schema.schemata
295
+ WHERE schema_name NOT IN ('information_schema', 'pg_catalog')
296
+ AND schema_name NOT LIKE 'pg_toast%'
297
+ AND schema_name NOT LIKE 'pg_temp_%'
298
+ `);
299
+ for (const { schema_name: schemaName } of result.rows) {
300
+ const quoted = `"${schemaName.replace(/"/g, '""')}"`;
301
+ await client.query(`DROP SCHEMA IF EXISTS ${quoted} CASCADE`);
302
+ }
303
+ await client.query('CREATE SCHEMA IF NOT EXISTS public');
304
+ });
192
305
  }
193
306
  // ── Classification sync ───────────────────────────────────────────────────────
194
307
  /**
@@ -281,20 +394,40 @@ function compileClassifications(classificationsFile, genJsonFile) {
281
394
  return false;
282
395
  }
283
396
  export async function createKysely(resolved) {
284
- mkdirSync(dirname(resolved.dbFile), { recursive: true });
285
- const runtime = await loadSqliteRuntime();
286
- let coercionMap;
287
- try {
288
- const mod = await import(resolved.coercionFile);
289
- coercionMap = mod.coercionMap;
397
+ const coercionMap = await loadCoercionPlugin(resolved.coercionFile);
398
+ const plugins = coercionMap
399
+ ? [createCoercionPlugin({ map: coercionMap })]
400
+ : [];
401
+ if (resolved.dialect === 'sqlite') {
402
+ mkdirSync(dirname(resolved.dbFile), { recursive: true });
403
+ const runtime = await loadSqliteRuntime();
404
+ return createSqliteKysely({
405
+ db: runtime.open(resolved.dbFile),
406
+ camelCase: resolved.camelCase,
407
+ plugins,
408
+ });
290
409
  }
291
- catch {
292
- // coercion.gen.ts not yet generated run `pikku db migrate` first
410
+ if (resolved.mode === 'url') {
411
+ const { Pool } = await import('pg');
412
+ const pool = new Pool({
413
+ connectionString: resolved.connectionString,
414
+ max: 10,
415
+ });
416
+ return new Kysely({
417
+ dialect: new PostgresDialect({ pool }),
418
+ plugins: resolved.camelCase
419
+ ? [new CamelCasePlugin(), ...plugins]
420
+ : plugins,
421
+ });
293
422
  }
294
- return createSqliteKysely({
295
- db: runtime.open(resolved.dbFile),
423
+ if (!resolved.pgliteDir) {
424
+ throw new Error('PGlite Postgres resolution is missing pgliteDir.');
425
+ }
426
+ mkdirSync(dirname(resolved.pgliteDir), { recursive: true });
427
+ return createPGliteKysely({
428
+ db: await createEmbeddedPostgres(resolved.pgliteDir),
296
429
  camelCase: resolved.camelCase,
297
- plugins: coercionMap ? [createCoercionPlugin({ map: coercionMap })] : [],
430
+ plugins,
298
431
  });
299
432
  }
300
433
  async function introspectorToMap(intro) {
@@ -308,8 +441,19 @@ async function introspectorToMap(intro) {
308
441
  function diffSchemas(desired, actual) {
309
442
  const missingTables = [];
310
443
  const missingColumns = [];
444
+ const findSchemaQualifiedMatch = (table) => {
445
+ if (table.includes('.'))
446
+ return undefined;
447
+ const matches = [...actual.entries()].filter(([actualTable]) => {
448
+ const parts = actualTable.split('.');
449
+ return parts.length === 2 && parts[1] === table;
450
+ });
451
+ if (matches.length !== 1)
452
+ return undefined;
453
+ return matches[0][1];
454
+ };
311
455
  for (const [table, cols] of desired) {
312
- const actualCols = actual.get(table);
456
+ const actualCols = actual.get(table) ?? findSchemaQualifiedMatch(table);
313
457
  if (!actualCols) {
314
458
  missingTables.push(table);
315
459
  continue;
@@ -323,60 +467,99 @@ function diffSchemas(desired, actual) {
323
467
  function isPostgresAuthDatabase(options) {
324
468
  return options.database?.type === 'postgres';
325
469
  }
326
- function createScratchPostgresSchemaName() {
470
+ function createScratchPostgresDatabaseName(prefix) {
327
471
  const random = Math.random().toString(36).slice(2, 10);
328
- return `pikku_auth_${Date.now().toString(36)}_${random}`;
472
+ return `${prefix}_${Date.now().toString(36)}_${random}`.slice(0, 63);
473
+ }
474
+ function quotePgIdentifier(identifier) {
475
+ return `"${identifier.replace(/"/g, '""')}"`;
329
476
  }
330
- async function postgresSchemaToMap(connectionString, schema) {
477
+ function withPostgresDatabase(connectionString, databaseName) {
478
+ const url = new URL(connectionString);
479
+ url.pathname = `/${databaseName}`;
480
+ return url.toString();
481
+ }
482
+ function getPostgresAdminConnectionString(connectionString) {
483
+ return withPostgresDatabase(connectionString, 'postgres');
484
+ }
485
+ async function withScratchPostgresDatabase(resolved, prefix, run) {
486
+ if (resolved.mode === 'pglite') {
487
+ const scratchDb = await createEmbeddedPostgres();
488
+ try {
489
+ return await run(pgliteAsClient(scratchDb));
490
+ }
491
+ finally {
492
+ if (!scratchDb.closed) {
493
+ await scratchDb.close();
494
+ }
495
+ }
496
+ }
331
497
  const { Client } = await import('pg');
332
- const client = new Client({ connectionString });
333
- await client.connect();
498
+ const databaseName = createScratchPostgresDatabaseName(prefix);
499
+ const adminConnectionString = getPostgresAdminConnectionString(resolved.connectionString);
500
+ const adminClient = new Client({ connectionString: adminConnectionString });
501
+ await adminClient.connect();
334
502
  try {
335
- const tablesResult = await client.query(`SELECT table_name
336
- FROM information_schema.tables
337
- WHERE table_schema = $1
338
- AND table_type = 'BASE TABLE'
339
- ORDER BY table_name`, [schema]);
340
- const map = new Map();
341
- for (const { table_name } of tablesResult.rows) {
342
- const columnsResult = await client.query(`SELECT column_name
343
- FROM information_schema.columns
344
- WHERE table_schema = $1
345
- AND table_name = $2
346
- ORDER BY ordinal_position`, [schema, table_name]);
347
- map.set(table_name, new Set(columnsResult.rows.map((c) => c.column_name)));
348
- }
349
- return map;
503
+ await adminClient.query(`CREATE DATABASE ${quotePgIdentifier(databaseName)}`);
350
504
  }
351
505
  finally {
352
- await client.end();
506
+ await adminClient.end();
353
507
  }
354
- }
355
- async function desiredPostgresAuthSchema(resolved, rootDir, srcDirectories, logger) {
356
- const { Pool } = await import('pg');
357
- const schema = createScratchPostgresSchemaName();
358
- const pool = new Pool({
359
- connectionString: resolved.connectionString,
360
- max: 1,
361
- });
508
+ const scratchConnectionString = withPostgresDatabase(resolved.connectionString, databaseName);
362
509
  try {
363
- const admin = await pool.connect();
510
+ const scratchClient = Object.assign(new Client({ connectionString: scratchConnectionString }), { __connectionString: scratchConnectionString });
511
+ await scratchClient.connect();
512
+ try {
513
+ return await run(scratchClient);
514
+ }
515
+ finally {
516
+ await scratchClient.end();
517
+ }
518
+ }
519
+ finally {
520
+ const cleanupClient = new Client({
521
+ connectionString: adminConnectionString,
522
+ });
523
+ await cleanupClient.connect();
364
524
  try {
365
- await admin.query(`CREATE SCHEMA "${schema}"`);
366
- await admin.query(`SET search_path TO "${schema}"`);
525
+ await cleanupClient.query(`SELECT pg_terminate_backend(pid)
526
+ FROM pg_stat_activity
527
+ WHERE datname = $1
528
+ AND pid <> pg_backend_pid()`, [databaseName]);
529
+ await cleanupClient.query(`DROP DATABASE IF EXISTS ${quotePgIdentifier(databaseName)}`);
367
530
  }
368
531
  finally {
369
- admin.release();
532
+ await cleanupClient.end();
370
533
  }
371
- const kysely = new Kysely({
372
- dialect: new PostgresDialect({
373
- pool,
374
- onReserveConnection: async (connection) => {
375
- await connection.executeQuery(CompiledQuery.raw(`SET search_path TO "${schema}"`));
376
- },
377
- }),
378
- plugins: [new CamelCasePlugin()],
379
- }).withSchema(schema);
534
+ }
535
+ }
536
+ async function postgresDatabaseToMap(client) {
537
+ const intro = new PostgresIntrospector(client);
538
+ await intro.connect();
539
+ try {
540
+ return await introspectorToMap(intro);
541
+ }
542
+ finally {
543
+ await intro.close();
544
+ }
545
+ }
546
+ async function desiredPostgresAuthSchema(resolved, rootDir, srcDirectories, logger) {
547
+ return withScratchPostgresDatabase(resolved, 'pikku_auth', async (scratchDb) => {
548
+ const { Pool } = await import('pg');
549
+ const kysely = resolved.mode === 'url'
550
+ ? new Kysely({
551
+ dialect: new PostgresDialect({
552
+ pool: new Pool({
553
+ connectionString: scratchDb.__connectionString,
554
+ max: 1,
555
+ }),
556
+ }),
557
+ plugins: [new CamelCasePlugin()],
558
+ })
559
+ : createPGliteKysely({
560
+ db: scratchDb.__pglite,
561
+ camelCase: true,
562
+ });
380
563
  try {
381
564
  const options = await loadAuthOptions({
382
565
  rootDir,
@@ -388,26 +571,14 @@ async function desiredPostgresAuthSchema(resolved, rootDir, srcDirectories, logg
388
571
  return null;
389
572
  const { runMigrations, compileMigrations } = await getAuthMigrations(options);
390
573
  await runMigrations();
391
- const tables = await postgresSchemaToMap(resolved.connectionString, schema);
574
+ const tables = await postgresDatabaseToMap(scratchDb);
392
575
  const sql = await compileMigrations();
393
576
  return { tables, sql };
394
577
  }
395
578
  finally {
396
579
  await kysely.destroy();
397
580
  }
398
- }
399
- finally {
400
- const cleanup = new Pool({
401
- connectionString: resolved.connectionString,
402
- max: 1,
403
- });
404
- try {
405
- await cleanup.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
406
- }
407
- finally {
408
- await cleanup.end();
409
- }
410
- }
581
+ });
411
582
  }
412
583
  export async function desiredAuthSchema(resolved, rootDir, srcDirectories, logger) {
413
584
  const runtime = await loadSqliteRuntime();
@@ -449,14 +620,16 @@ export async function introspectSchema(resolved) {
449
620
  db.close();
450
621
  }
451
622
  }
452
- const intro = new PostgresIntrospector(resolved.connectionString);
453
- await intro.connect();
454
- try {
455
- return await introspectorToMap(intro);
456
- }
457
- finally {
458
- await intro.close();
459
- }
623
+ return withPostgresClient(resolved, async (client) => {
624
+ const intro = new PostgresIntrospector(client);
625
+ await intro.connect();
626
+ try {
627
+ return await introspectorToMap(intro);
628
+ }
629
+ finally {
630
+ await intro.close();
631
+ }
632
+ });
460
633
  }
461
634
  async function coveredSqliteSchema(migrationsDir) {
462
635
  const runtime = await loadSqliteRuntime();
@@ -469,6 +642,12 @@ async function coveredSqliteSchema(migrationsDir) {
469
642
  db.close();
470
643
  }
471
644
  }
645
+ async function coveredPostgresSchema(resolved, migrationsDir) {
646
+ return withScratchPostgresDatabase(resolved, 'pikku_migrate', async (client) => {
647
+ await migrate(new PostgresMigrationExecutor(client), migrationsDir);
648
+ return postgresDatabaseToMap(client);
649
+ });
650
+ }
472
651
  export async function computeAuthDrift(resolved, rootDir, srcDirectories, logger) {
473
652
  const desired = await desiredAuthSchema(resolved, rootDir, srcDirectories, logger);
474
653
  if (!desired) {
@@ -505,12 +684,12 @@ function nextMigrationFile(migrationsDir, label) {
505
684
  return join(migrationsDir, `${num}-${label}.sql`);
506
685
  }
507
686
  export async function generateAuthMigration(resolved, rootDir, srcDirectories, logger) {
508
- if (resolved.dialect !== 'sqlite')
509
- return { status: 'unsupported-dialect' };
510
687
  const desired = await desiredAuthSchema(resolved, rootDir, srcDirectories, logger);
511
688
  if (!desired)
512
689
  return { status: 'no-auth' };
513
- const covered = await coveredSqliteSchema(resolved.migrationsDir);
690
+ const covered = resolved.dialect === 'sqlite'
691
+ ? await coveredSqliteSchema(resolved.migrationsDir)
692
+ : await coveredPostgresSchema(resolved, resolved.migrationsDir);
514
693
  const { missingTables, missingColumns } = diffSchemas(desired.tables, covered);
515
694
  if (missingTables.length === 0 && missingColumns.length === 0) {
516
695
  return { status: 'up-to-date' };
@@ -0,0 +1,8 @@
1
+ import { Kysely, type KyselyPlugin } from 'kysely';
2
+ import type { PGlite } from '@electric-sql/pglite';
3
+ export interface CreatePGliteKyselyOptions {
4
+ db: PGlite;
5
+ camelCase?: boolean;
6
+ plugins?: KyselyPlugin[];
7
+ }
8
+ export declare function createPGliteKysely<DB>(options: CreatePGliteKyselyOptions): Kysely<DB>;
@@ -0,0 +1,79 @@
1
+ import { CamelCasePlugin, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, } from 'kysely';
2
+ export function createPGliteKysely(options) {
3
+ const plugins = [];
4
+ if (options.camelCase ?? true)
5
+ plugins.push(new CamelCasePlugin());
6
+ if (options.plugins)
7
+ plugins.push(...options.plugins);
8
+ return new Kysely({
9
+ dialect: new PGliteDialect(options.db),
10
+ plugins,
11
+ });
12
+ }
13
+ class PGliteDialect {
14
+ db;
15
+ constructor(db) {
16
+ this.db = db;
17
+ }
18
+ createAdapter() {
19
+ return new PostgresAdapter();
20
+ }
21
+ createDriver() {
22
+ return new PGliteDriver(this.db);
23
+ }
24
+ createQueryCompiler() {
25
+ return new PostgresQueryCompiler();
26
+ }
27
+ createIntrospector(db) {
28
+ return new PostgresIntrospector(db);
29
+ }
30
+ }
31
+ class PGliteDriver {
32
+ connection;
33
+ constructor(db) {
34
+ this.connection = new PGliteConnection(db);
35
+ }
36
+ async init() { }
37
+ async acquireConnection() {
38
+ return this.connection;
39
+ }
40
+ async beginTransaction(conn) {
41
+ await conn.executeRaw('BEGIN');
42
+ }
43
+ async commitTransaction(conn) {
44
+ await conn.executeRaw('COMMIT');
45
+ }
46
+ async rollbackTransaction(conn) {
47
+ await conn.executeRaw('ROLLBACK');
48
+ }
49
+ async releaseConnection() { }
50
+ async destroy() {
51
+ await this.connection.close();
52
+ }
53
+ }
54
+ class PGliteConnection {
55
+ db;
56
+ constructor(db) {
57
+ this.db = db;
58
+ }
59
+ async executeQuery(query) {
60
+ const result = await this.db.query(query.sql, [...query.parameters]);
61
+ return {
62
+ rows: result.rows,
63
+ numAffectedRows: result.affectedRows !== undefined
64
+ ? BigInt(result.affectedRows)
65
+ : undefined,
66
+ };
67
+ }
68
+ async *streamQuery(query) {
69
+ yield await this.executeQuery(query);
70
+ }
71
+ async executeRaw(sql) {
72
+ await this.db.exec(sql);
73
+ }
74
+ async close() {
75
+ if (!this.db.closed) {
76
+ await this.db.close();
77
+ }
78
+ }
79
+ }
@@ -7,6 +7,7 @@ interface QueryClient {
7
7
  }
8
8
  export declare class PostgresIntrospector implements DbIntrospector {
9
9
  private client;
10
+ private ownsClient;
10
11
  constructor(clientOrConnectionString: QueryClient | string);
11
12
  connect(): Promise<void>;
12
13
  listTables(): Promise<string[]>;
@@ -1,15 +1,18 @@
1
1
  import { Pool } from 'pg';
2
2
  export class PostgresIntrospector {
3
3
  client;
4
+ ownsClient;
4
5
  constructor(clientOrConnectionString) {
5
6
  if (typeof clientOrConnectionString === 'string') {
6
7
  this.client = new Pool({
7
8
  connectionString: clientOrConnectionString,
8
9
  max: 10,
9
10
  });
11
+ this.ownsClient = true;
10
12
  }
11
13
  else {
12
14
  this.client = clientOrConnectionString;
15
+ this.ownsClient = false;
13
16
  }
14
17
  }
15
18
  async connect() {
@@ -106,6 +109,8 @@ export class PostgresIntrospector {
106
109
  }));
107
110
  }
108
111
  async close() {
109
- await this.client.end();
112
+ if (this.ownsClient) {
113
+ await this.client.end();
114
+ }
110
115
  }
111
116
  }
@@ -1,8 +1,13 @@
1
- import type { Client } from 'pg';
2
1
  import type { MigrationExecutor, AppliedMigration } from '../db-migrator.js';
2
+ export interface PostgresMigrationClient {
3
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<{
4
+ rows: T[];
5
+ }>;
6
+ exec?(sql: string): Promise<unknown>;
7
+ }
3
8
  export declare class PostgresMigrationExecutor implements MigrationExecutor {
4
9
  private readonly client;
5
- constructor(client: Client);
10
+ constructor(client: PostgresMigrationClient);
6
11
  ensureTrackingTable(): Promise<void>;
7
12
  getApplied(): Promise<AppliedMigration[]>;
8
13
  runMigration(sql: string, name: string, hash: string): Promise<void>;