@pikku/cli 0.12.39 → 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 +170 -170
  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 +7 -7
  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 +275 -107
  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
@@ -24,10 +24,8 @@ export async function loadUserConfigForDb(options) {
24
24
  const getFallbackConfig = () => {
25
25
  if (hasSqliteDbAssets)
26
26
  return { sqliteDb: '.pikku-runtime/dev.db' };
27
- if (hasPostgresDbAssets) {
28
- logger.error('Postgres assets detected but postgresUrl is not configured in createConfig.');
29
- return null;
30
- }
27
+ if (hasPostgresDbAssets)
28
+ return {};
31
29
  return null;
32
30
  };
33
31
  const configFactoryFile = findUserConfigFactoryFile(config.rootDir, config.srcDirectories);
@@ -179,6 +179,7 @@ export const deployApply = pikkuSessionlessFunc({
179
179
  inspectorState,
180
180
  serverlessIncompatible: config.deploy?.serverlessIncompatible,
181
181
  getEntryContext,
182
+ outDir: config.outDir,
182
183
  logger,
183
184
  });
184
185
  if (buildResult.manifest.units.length === 0) {
@@ -55,6 +55,7 @@ export const deployPlan = pikkuSessionlessFunc({
55
55
  inspectorState,
56
56
  serverlessIncompatible: config.deploy?.serverlessIncompatible,
57
57
  getEntryContext,
58
+ outDir: config.outDir,
58
59
  logger,
59
60
  });
60
61
  if (result.manifest.units.length === 0) {
@@ -113,7 +113,7 @@ export const dev = pikkuSessionlessFunc({
113
113
  ? parseDatabaseUrl(envDatabaseUrl)
114
114
  : userConfig;
115
115
  const resolvedDb = resolveDb(effectiveDbConfig, config.rootDir, config.outDir, config.runtimeDir);
116
- const resolvedLocalDb = resolvedDb?.dialect === 'sqlite' ? resolvedDb : undefined;
116
+ const resolvedLocalDb = resolvedDb ?? undefined;
117
117
  const kysely = resolvedLocalDb
118
118
  ? await createKysely(resolvedLocalDb)
119
119
  : undefined;
@@ -25,7 +25,11 @@ export interface ResolvedSqliteDb extends ResolvedDbBase {
25
25
  }
26
26
  export interface ResolvedPostgresDb extends ResolvedDbBase {
27
27
  dialect: 'postgres';
28
- connectionString: string;
28
+ mode: 'url' | 'pglite';
29
+ connectionString?: string;
30
+ pgliteDir?: string;
31
+ runtimeDir: string;
32
+ seedFile: string;
29
33
  }
30
34
  export type ResolvedDb = ResolvedSqliteDb | ResolvedPostgresDb;
31
35
  /**
@@ -50,9 +54,9 @@ export interface MigrateAndCodegenOutcome {
50
54
  classificationsJsonWritten: boolean;
51
55
  }
52
56
  export declare function migrateAndCodegen(resolved: ResolvedDb): Promise<MigrateAndCodegenOutcome>;
53
- export declare function seed(resolved: ResolvedSqliteDb): Promise<SeedResult>;
54
- export declare function reset(resolved: ResolvedSqliteDb, rootDir: string): void;
55
- export declare function createKysely<DB>(resolved: ResolvedSqliteDb): Promise<Kysely<DB>>;
57
+ export declare function seed(resolved: ResolvedDb): Promise<SeedResult>;
58
+ export declare function reset(resolved: ResolvedDb, rootDir: string): Promise<void>;
59
+ export declare function createKysely<DB>(resolved: ResolvedDb): Promise<Kysely<DB>>;
56
60
  type SchemaMap = Map<string, Set<string>>;
57
61
  export interface DesiredAuthSchema {
58
62
  tables: SchemaMap;
@@ -75,7 +79,7 @@ export declare function computeAuthDrift(resolved: ResolvedDb, rootDir: string,
75
79
  error: (msg: string) => void;
76
80
  }): Promise<AuthDriftResult>;
77
81
  export interface GenerateAuthResult {
78
- status: 'no-auth' | 'up-to-date' | 'written' | 'incremental-unsupported' | 'unsupported-dialect';
82
+ status: 'no-auth' | 'up-to-date' | 'written' | 'incremental-unsupported';
79
83
  file?: string;
80
84
  missingTables?: string[];
81
85
  missingColumns?: {
@@ -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
+ });
422
+ }
423
+ if (!resolved.pgliteDir) {
424
+ throw new Error('PGlite Postgres resolution is missing pgliteDir.');
293
425
  }
294
- return createSqliteKysely({
295
- db: runtime.open(resolved.dbFile),
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) {
@@ -334,60 +467,99 @@ function diffSchemas(desired, actual) {
334
467
  function isPostgresAuthDatabase(options) {
335
468
  return options.database?.type === 'postgres';
336
469
  }
337
- function createScratchPostgresSchemaName() {
470
+ function createScratchPostgresDatabaseName(prefix) {
338
471
  const random = Math.random().toString(36).slice(2, 10);
339
- 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, '""')}"`;
476
+ }
477
+ function withPostgresDatabase(connectionString, databaseName) {
478
+ const url = new URL(connectionString);
479
+ url.pathname = `/${databaseName}`;
480
+ return url.toString();
340
481
  }
341
- async function postgresSchemaToMap(connectionString, schema) {
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
+ }
342
497
  const { Client } = await import('pg');
343
- const client = new Client({ connectionString });
344
- 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();
345
502
  try {
346
- const tablesResult = await client.query(`SELECT table_name
347
- FROM information_schema.tables
348
- WHERE table_schema = $1
349
- AND table_type = 'BASE TABLE'
350
- ORDER BY table_name`, [schema]);
351
- const map = new Map();
352
- for (const { table_name } of tablesResult.rows) {
353
- const columnsResult = await client.query(`SELECT column_name
354
- FROM information_schema.columns
355
- WHERE table_schema = $1
356
- AND table_name = $2
357
- ORDER BY ordinal_position`, [schema, table_name]);
358
- map.set(table_name, new Set(columnsResult.rows.map((c) => c.column_name)));
359
- }
360
- return map;
503
+ await adminClient.query(`CREATE DATABASE ${quotePgIdentifier(databaseName)}`);
361
504
  }
362
505
  finally {
363
- await client.end();
506
+ await adminClient.end();
364
507
  }
365
- }
366
- async function desiredPostgresAuthSchema(resolved, rootDir, srcDirectories, logger) {
367
- const { Pool } = await import('pg');
368
- const schema = createScratchPostgresSchemaName();
369
- const pool = new Pool({
370
- connectionString: resolved.connectionString,
371
- max: 1,
372
- });
508
+ const scratchConnectionString = withPostgresDatabase(resolved.connectionString, databaseName);
373
509
  try {
374
- const admin = await pool.connect();
510
+ const scratchClient = Object.assign(new Client({ connectionString: scratchConnectionString }), { __connectionString: scratchConnectionString });
511
+ await scratchClient.connect();
375
512
  try {
376
- await admin.query(`CREATE SCHEMA "${schema}"`);
377
- await admin.query(`SET search_path TO "${schema}"`);
513
+ return await run(scratchClient);
378
514
  }
379
515
  finally {
380
- admin.release();
516
+ await scratchClient.end();
381
517
  }
382
- const kysely = new Kysely({
383
- dialect: new PostgresDialect({
384
- pool,
385
- onReserveConnection: async (connection) => {
386
- await connection.executeQuery(CompiledQuery.raw(`SET search_path TO "${schema}"`));
387
- },
388
- }),
389
- plugins: [new CamelCasePlugin()],
390
- }).withSchema(schema);
518
+ }
519
+ finally {
520
+ const cleanupClient = new Client({
521
+ connectionString: adminConnectionString,
522
+ });
523
+ await cleanupClient.connect();
524
+ try {
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)}`);
530
+ }
531
+ finally {
532
+ await cleanupClient.end();
533
+ }
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
+ });
391
563
  try {
392
564
  const options = await loadAuthOptions({
393
565
  rootDir,
@@ -399,26 +571,14 @@ async function desiredPostgresAuthSchema(resolved, rootDir, srcDirectories, logg
399
571
  return null;
400
572
  const { runMigrations, compileMigrations } = await getAuthMigrations(options);
401
573
  await runMigrations();
402
- const tables = await postgresSchemaToMap(resolved.connectionString, schema);
574
+ const tables = await postgresDatabaseToMap(scratchDb);
403
575
  const sql = await compileMigrations();
404
576
  return { tables, sql };
405
577
  }
406
578
  finally {
407
579
  await kysely.destroy();
408
580
  }
409
- }
410
- finally {
411
- const cleanup = new Pool({
412
- connectionString: resolved.connectionString,
413
- max: 1,
414
- });
415
- try {
416
- await cleanup.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
417
- }
418
- finally {
419
- await cleanup.end();
420
- }
421
- }
581
+ });
422
582
  }
423
583
  export async function desiredAuthSchema(resolved, rootDir, srcDirectories, logger) {
424
584
  const runtime = await loadSqliteRuntime();
@@ -460,14 +620,16 @@ export async function introspectSchema(resolved) {
460
620
  db.close();
461
621
  }
462
622
  }
463
- const intro = new PostgresIntrospector(resolved.connectionString);
464
- await intro.connect();
465
- try {
466
- return await introspectorToMap(intro);
467
- }
468
- finally {
469
- await intro.close();
470
- }
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
+ });
471
633
  }
472
634
  async function coveredSqliteSchema(migrationsDir) {
473
635
  const runtime = await loadSqliteRuntime();
@@ -480,6 +642,12 @@ async function coveredSqliteSchema(migrationsDir) {
480
642
  db.close();
481
643
  }
482
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
+ }
483
651
  export async function computeAuthDrift(resolved, rootDir, srcDirectories, logger) {
484
652
  const desired = await desiredAuthSchema(resolved, rootDir, srcDirectories, logger);
485
653
  if (!desired) {
@@ -516,12 +684,12 @@ function nextMigrationFile(migrationsDir, label) {
516
684
  return join(migrationsDir, `${num}-${label}.sql`);
517
685
  }
518
686
  export async function generateAuthMigration(resolved, rootDir, srcDirectories, logger) {
519
- if (resolved.dialect !== 'sqlite')
520
- return { status: 'unsupported-dialect' };
521
687
  const desired = await desiredAuthSchema(resolved, rootDir, srcDirectories, logger);
522
688
  if (!desired)
523
689
  return { status: 'no-auth' };
524
- const covered = await coveredSqliteSchema(resolved.migrationsDir);
690
+ const covered = resolved.dialect === 'sqlite'
691
+ ? await coveredSqliteSchema(resolved.migrationsDir)
692
+ : await coveredPostgresSchema(resolved, resolved.migrationsDir);
525
693
  const { missingTables, missingColumns } = diffSchemas(desired.tables, covered);
526
694
  if (missingTables.length === 0 && missingColumns.length === 0) {
527
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>;