@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.
- package/console-app/assets/{index-Dxl3JsMK.js → index-D9Z9rySK.js} +2 -2
- package/console-app/index.html +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +135 -135
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +3 -3
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +5 -5
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/deploy/build-pipeline.d.ts +1 -0
- package/dist/src/deploy/build-pipeline.js +1 -1
- package/dist/src/fabric/functions/validate-core.js +2 -7
- package/dist/src/fabric/functions/validate.function.js +16 -14
- package/dist/src/functions/commands/db-generate.js +0 -3
- package/dist/src/functions/commands/db-reset.js +11 -7
- package/dist/src/functions/commands/db-seed.js +4 -7
- package/dist/src/functions/commands/db-shared.js +2 -4
- package/dist/src/functions/commands/deploy-apply.js +1 -0
- package/dist/src/functions/commands/deploy-plan.js +1 -0
- package/dist/src/functions/commands/dev.js +1 -1
- package/dist/src/functions/db/local-db.d.ts +9 -5
- package/dist/src/functions/db/local-db.js +287 -108
- package/dist/src/functions/db/postgres/pglite-kysely.d.ts +8 -0
- package/dist/src/functions/db/postgres/pglite-kysely.js +79 -0
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +1 -0
- package/dist/src/functions/db/postgres/postgres-introspector.js +6 -1
- package/dist/src/functions/db/postgres/postgres-migrator.d.ts +7 -2
- package/dist/src/functions/db/postgres/postgres-migrator.js +6 -1
- package/dist/src/functions/validate/workspace-validate.js +4 -4
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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,
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
|
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
|
|
470
|
+
function createScratchPostgresDatabaseName(prefix) {
|
|
327
471
|
const random = Math.random().toString(36).slice(2, 10);
|
|
328
|
-
return
|
|
472
|
+
return `${prefix}_${Date.now().toString(36)}_${random}`.slice(0, 63);
|
|
473
|
+
}
|
|
474
|
+
function quotePgIdentifier(identifier) {
|
|
475
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
329
476
|
}
|
|
330
|
-
|
|
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
|
|
333
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
366
|
-
|
|
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
|
-
|
|
532
|
+
await cleanupClient.end();
|
|
370
533
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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>;
|