@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.
- 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 +170 -170
- 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 +7 -7
- 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 +275 -107
- 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
|
@@ -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
|
-
|
|
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) {
|
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
54
|
-
export declare function reset(resolved:
|
|
55
|
-
export declare function createKysely<DB>(resolved:
|
|
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'
|
|
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,
|
|
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
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (!resolved.pgliteDir) {
|
|
424
|
+
throw new Error('PGlite Postgres resolution is missing pgliteDir.');
|
|
293
425
|
}
|
|
294
|
-
|
|
295
|
-
|
|
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) {
|
|
@@ -334,60 +467,99 @@ function diffSchemas(desired, actual) {
|
|
|
334
467
|
function isPostgresAuthDatabase(options) {
|
|
335
468
|
return options.database?.type === 'postgres';
|
|
336
469
|
}
|
|
337
|
-
function
|
|
470
|
+
function createScratchPostgresDatabaseName(prefix) {
|
|
338
471
|
const random = Math.random().toString(36).slice(2, 10);
|
|
339
|
-
return
|
|
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
|
-
|
|
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
|
|
344
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
510
|
+
const scratchClient = Object.assign(new Client({ connectionString: scratchConnectionString }), { __connectionString: scratchConnectionString });
|
|
511
|
+
await scratchClient.connect();
|
|
375
512
|
try {
|
|
376
|
-
await
|
|
377
|
-
await admin.query(`SET search_path TO "${schema}"`);
|
|
513
|
+
return await run(scratchClient);
|
|
378
514
|
}
|
|
379
515
|
finally {
|
|
380
|
-
|
|
516
|
+
await scratchClient.end();
|
|
381
517
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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 =
|
|
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>;
|