@pikku/cli 0.12.37 → 0.12.38
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/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 +129 -129
- 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 +1 -1
- 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/fabric/functions/validate-core.js +7 -7
- package/dist/src/fabric/functions/validate.function.js +36 -28
- package/dist/src/fabric/lib/config.d.ts +4 -4
- package/dist/src/fabric/lib/config.js +2 -2
- package/dist/src/functions/db/better-auth-schema.js +33 -11
- package/dist/src/functions/db/db-codegen.js +9 -5
- package/dist/src/functions/db/local-db.d.ts +2 -2
- package/dist/src/functions/db/local-db.js +111 -5
- package/dist/src/functions/db/sqlite/sqlite-runtime-node.js +2 -1
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* This file was generated by @pikku/cli@0.12.
|
|
2
|
+
* This file was generated by @pikku/cli@0.12.38
|
|
3
3
|
*/
|
|
4
4
|
export { wireVariable } from '@pikku/core/variable';
|
|
5
5
|
export type { CoreVariable, VariableDefinitionMeta, VariableDefinitionsMeta } from '@pikku/core/variable';
|
package/dist/bin/pikku-bin.mjs
CHANGED
|
@@ -11,8 +11,8 @@ async function checkForUpdate() {
|
|
|
11
11
|
})
|
|
12
12
|
if (!res.ok) return
|
|
13
13
|
const { version: latest } = await res.json()
|
|
14
|
-
if (latest !== '0.12.
|
|
15
|
-
process.stderr.write(`\n Update available 0.12.
|
|
14
|
+
if (latest !== '0.12.38') {
|
|
15
|
+
process.stderr.write(`\n Update available 0.12.38 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
|
|
16
16
|
}
|
|
17
17
|
} catch {}
|
|
18
18
|
}
|
|
@@ -79,16 +79,16 @@ export async function runFabricValidate(startDir = process.cwd()) {
|
|
|
79
79
|
catch {
|
|
80
80
|
w('gitignore-missing', '.gitignore not found at project root — generated artifacts (.pikku, .pikku-runtime, .opencode) may be committed accidentally', gitignorePath, 'Create .gitignore and add ".pikku", ".pikku-runtime", and ".opencode" to it');
|
|
81
81
|
}
|
|
82
|
-
const fabricConfigPath = join(root, '
|
|
82
|
+
const fabricConfigPath = join(root, 'pikkufabric.config.json');
|
|
83
83
|
const fabricConfig = await readJsonSafe(fabricConfigPath);
|
|
84
84
|
if (!fabricConfig) {
|
|
85
|
-
info('fabric-config-missing', '
|
|
85
|
+
info('fabric-config-missing', 'pikkufabric.config.json not found — project has not been linked to fabric yet', fabricConfigPath, 'Run `pikku fabric link` to create it, or create manually: {"projectId": "__PROJECT_ID__"}');
|
|
86
86
|
}
|
|
87
87
|
else if (!fabricConfig.projectId) {
|
|
88
|
-
info('fabric-config-no-project-id', '
|
|
88
|
+
info('fabric-config-no-project-id', 'pikkufabric.config.json is missing "projectId"', fabricConfigPath, 'Add "projectId": "<your-project-id>" to pikkufabric.config.json, or run `pikku fabric link`');
|
|
89
89
|
}
|
|
90
90
|
else if (fabricConfig.projectId === '__PROJECT_ID__') {
|
|
91
|
-
info('fabric-config-placeholder-project-id', '
|
|
91
|
+
info('fabric-config-placeholder-project-id', 'pikkufabric.config.json has a placeholder projectId ("__PROJECT_ID__") — project is not linked', fabricConfigPath, 'Run `pikku fabric link` to replace the placeholder with a real project ID');
|
|
92
92
|
}
|
|
93
93
|
const rootPkgPath = join(root, 'package.json');
|
|
94
94
|
const rootPkg = await readJsonSafe(rootPkgPath);
|
|
@@ -103,7 +103,7 @@ export async function runFabricValidate(startDir = process.cwd()) {
|
|
|
103
103
|
}
|
|
104
104
|
const fnDir = join(root, 'packages', 'functions');
|
|
105
105
|
const functionsSdkPkgName = (await readJsonSafe(join(root, 'packages', 'functions-sdk', 'package.json')))?.name;
|
|
106
|
-
const themePkgName = (await readJsonSafe(join(root, 'packages', 'theme', 'package.json')))?.name;
|
|
106
|
+
const themePkgName = (await readJsonSafe(join(root, 'packages', 'mantine-theme', 'package.json')))?.name;
|
|
107
107
|
const componentsPkgName = (await readJsonSafe(join(root, 'packages', 'components', 'package.json')))?.name;
|
|
108
108
|
if (existsSync(fnDir)) {
|
|
109
109
|
const fnPkgPath = join(fnDir, 'package.json');
|
|
@@ -241,8 +241,8 @@ export async function runFabricValidate(startDir = process.cwd()) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
const designDocUrl = 'https://pikkufabric.dev/docs/design';
|
|
244
|
-
if (!existsSync(join(root, 'packages', 'theme'))) {
|
|
245
|
-
info('theme-missing', 'packages/theme/ not found — Fabric design features require a theme package', join(root, 'packages', 'theme'), `Create packages/theme/ with your Mantine theme tokens. See ${designDocUrl}`);
|
|
244
|
+
if (!existsSync(join(root, 'packages', 'mantine-theme'))) {
|
|
245
|
+
info('theme-missing', 'packages/mantine-theme/ not found — Fabric design features require a theme package', join(root, 'packages', 'mantine-theme'), `Create packages/mantine-theme/ with your Mantine theme tokens. See ${designDocUrl}`);
|
|
246
246
|
}
|
|
247
247
|
if (!existsSync(join(root, 'packages', 'components'))) {
|
|
248
248
|
info('components-missing', 'packages/components/ not found — Fabric design features require a components package', join(root, 'packages', 'components'), `Create packages/components/ with your shared UI components. See ${designDocUrl}`);
|
|
@@ -20,8 +20,9 @@ export const FabricValidateOutput = z.object({
|
|
|
20
20
|
async function findProjectRoot(startDir) {
|
|
21
21
|
let dir = startDir;
|
|
22
22
|
while (true) {
|
|
23
|
-
if (existsSync(join(dir, '
|
|
23
|
+
if (existsSync(join(dir, 'pikkufabric.config.json'))) {
|
|
24
24
|
return dir;
|
|
25
|
+
}
|
|
25
26
|
if (existsSync(join(dir, 'package.json'))) {
|
|
26
27
|
try {
|
|
27
28
|
const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
|
|
@@ -83,19 +84,19 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
83
84
|
const info = (id, message, path, fixHint) => {
|
|
84
85
|
findings.push({ id, severity: 'info', message, path, fixHint });
|
|
85
86
|
};
|
|
86
|
-
// ──
|
|
87
|
+
// ── pikkufabric.config.json ────────────────────────────────────────────
|
|
87
88
|
// Not required to run validate — downgraded to info so any pikku project
|
|
88
89
|
// can be checked for compatibility before it is linked to a fabric account.
|
|
89
|
-
const fabricConfigPath = join(root, '
|
|
90
|
+
const fabricConfigPath = join(root, 'pikkufabric.config.json');
|
|
90
91
|
const fabricConfig = await readJsonSafe(fabricConfigPath);
|
|
91
92
|
if (!fabricConfig) {
|
|
92
|
-
info('fabric-config-missing', '
|
|
93
|
+
info('fabric-config-missing', 'pikkufabric.config.json not found — project has not been linked to fabric yet', fabricConfigPath, 'Run `pikku fabric link` to create it, or create manually: {"projectId": "__PROJECT_ID__"}');
|
|
93
94
|
}
|
|
94
95
|
else if (!fabricConfig.projectId) {
|
|
95
|
-
info('fabric-config-no-project-id', '
|
|
96
|
+
info('fabric-config-no-project-id', 'pikkufabric.config.json is missing "projectId"', fabricConfigPath, 'Add "projectId": "<your-project-id>" to pikkufabric.config.json, or run `pikku fabric link`');
|
|
96
97
|
}
|
|
97
98
|
else if (fabricConfig.projectId === '__PROJECT_ID__') {
|
|
98
|
-
info('fabric-config-placeholder-project-id', '
|
|
99
|
+
info('fabric-config-placeholder-project-id', 'pikkufabric.config.json has a placeholder projectId ("__PROJECT_ID__") — project is not linked', fabricConfigPath, 'Run `pikku fabric link` to replace the placeholder with a real project ID');
|
|
99
100
|
}
|
|
100
101
|
// ── root pikku.config.json ─────────────────────────────────────────────
|
|
101
102
|
const pikkuConfigPath = join(root, 'pikku.config.json');
|
|
@@ -114,6 +115,7 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
114
115
|
info('pikku-config-no-client-files', 'pikku.config.json missing "clientFiles" — no generated SDK or React Query hooks', pikkuConfigPath, 'Add clientFiles.rpcMapDeclarationFile and clientFiles.reactQueryFile pointing to packages/functions-sdk/src/pikku/');
|
|
115
116
|
}
|
|
116
117
|
}
|
|
118
|
+
const dbEngine = pikkuConfig?.db?.engine ?? 'sqlite';
|
|
117
119
|
const rootPkgPath = join(root, 'package.json');
|
|
118
120
|
const rootPkg = await readJsonSafe(rootPkgPath);
|
|
119
121
|
if (!rootPkg) {
|
|
@@ -149,7 +151,7 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
149
151
|
// ── packages/functions/ ────────────────────────────────────────────────
|
|
150
152
|
const fnDir = join(root, 'packages', 'functions');
|
|
151
153
|
const functionsSdkPkgName = (await readJsonSafe(join(root, 'packages', 'functions-sdk', 'package.json')))?.name;
|
|
152
|
-
const themePkgName = (await readJsonSafe(join(root, 'packages', 'theme', 'package.json')))?.name;
|
|
154
|
+
const themePkgName = (await readJsonSafe(join(root, 'packages', 'mantine-theme', 'package.json')))?.name;
|
|
153
155
|
const componentsPkgName = (await readJsonSafe(join(root, 'packages', 'components', 'package.json')))?.name;
|
|
154
156
|
if (!existsSync(fnDir)) {
|
|
155
157
|
e('functions-pkg-missing', 'packages/functions/ directory not found', fnDir, 'Create packages/functions/ as a yarn workspace containing pikku.config.json, src/, and db/sqlite/');
|
|
@@ -170,7 +172,7 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
170
172
|
...fnPkg.devDependencies,
|
|
171
173
|
...fnPkg.peerDependencies,
|
|
172
174
|
};
|
|
173
|
-
if (fnAllDeps['@pikku/kysely-postgres']) {
|
|
175
|
+
if (dbEngine !== 'postgres' && fnAllDeps['@pikku/kysely-postgres']) {
|
|
174
176
|
e('fn-pkg-postgres-dep', '@pikku/kysely-postgres is in packages/functions dependencies — Fabric uses SQLite/libSQL (Turso), not PostgreSQL', fnPkgPath, 'Remove @pikku/kysely-postgres and use @pikku/kysely-sqlite with LibsqlWebDialect instead');
|
|
175
177
|
}
|
|
176
178
|
}
|
|
@@ -185,25 +187,26 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
185
187
|
const usesLibsql = servicesText.includes('@pikku/kysely-sqlite') ||
|
|
186
188
|
servicesText.includes('LibsqlWebDialect');
|
|
187
189
|
const usesProcessEnv = /\bprocess\.env\.[A-Z_]/.test(servicesText);
|
|
188
|
-
if (usesKysely && !usesLibsql) {
|
|
190
|
+
if (dbEngine !== 'postgres' && usesKysely && !usesLibsql) {
|
|
189
191
|
e('services-wrong-db-adapter', 'services.ts uses Kysely but not LibsqlWebDialect — Fabric injects a Turso/libSQL DATABASE_URL at runtime, not a PostgreSQL URL', servicesPath, 'Import LibsqlWebDialect from @pikku/kysely-sqlite and replace the dialect: new Kysely({ dialect: new LibsqlWebDialect({ url: databaseUrl }) })');
|
|
190
192
|
}
|
|
191
193
|
if (usesProcessEnv) {
|
|
192
194
|
info('services-process-env', 'services.ts reads process.env directly — prefer variables.get() for portable secret/variable access', servicesPath, 'Replace process.env.SOME_VAR with await variables.get("SOME_VAR") — declare the binding with wireVariable/wireSecret; process.env is fine for optional/non-secret config');
|
|
193
195
|
}
|
|
194
|
-
if (
|
|
196
|
+
if (dbEngine !== 'postgres' &&
|
|
197
|
+
usesLibsql &&
|
|
195
198
|
rootPkg &&
|
|
196
199
|
!rootPkg.dependencies?.['@pikku/kysely-sqlite'] &&
|
|
197
200
|
!rootPkg.devDependencies?.['@pikku/kysely-sqlite']) {
|
|
198
201
|
e('missing-kysely-sqlite', 'services.ts imports @pikku/kysely-sqlite but it is not in root package.json', rootPkgPath, 'Add "@pikku/kysely-sqlite": "file:./vendor/pikku-kysely-sqlite.tgz" to dependencies');
|
|
199
202
|
}
|
|
200
203
|
}
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
// db/sqlite (SQLite/libSQL) or db/postgres (PostgreSQL).
|
|
204
|
-
const migrationsDir = join(root, 'db', 'sqlite');
|
|
204
|
+
// Database layout is declared by pikku.config.json db.engine.
|
|
205
|
+
const migrationsDir = join(root, 'db', dbEngine === 'postgres' ? 'postgres' : 'sqlite');
|
|
205
206
|
if (!existsSync(migrationsDir)) {
|
|
206
|
-
e('migrations-dir-missing',
|
|
207
|
+
e('migrations-dir-missing', `db/${dbEngine === 'postgres' ? 'postgres' : 'sqlite'}/ not found`, migrationsDir, dbEngine === 'postgres'
|
|
208
|
+
? 'Create db/postgres/ and add numbered .sql files (e.g. 0001-init.sql) using PostgreSQL-compatible syntax'
|
|
209
|
+
: 'Create db/sqlite/ and add numbered .sql files (e.g. 0001-init.sql) using SQLite-compatible syntax');
|
|
207
210
|
}
|
|
208
211
|
else {
|
|
209
212
|
try {
|
|
@@ -223,14 +226,16 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
223
226
|
break;
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
if (dbEngine !== 'postgres') {
|
|
230
|
+
// Check for PostgreSQL-specific syntax — Fabric uses Turso (SQLite/libSQL)
|
|
231
|
+
for (const f of files) {
|
|
232
|
+
const sql = await readTextSafe(join(migrationsDir, f));
|
|
233
|
+
if (!sql)
|
|
234
|
+
continue;
|
|
235
|
+
const hits = POSTGRES_SQL_PATTERNS.filter(({ re }) => re.test(sql)).map(({ label }) => label);
|
|
236
|
+
if (hits.length > 0) {
|
|
237
|
+
e(`migration-postgres-sql-${f.replace(/[^a-z0-9]/gi, '-')}`, `${f} contains PostgreSQL syntax (${hits.join(', ')}) — Fabric uses SQLite/libSQL (Turso)`, join(migrationsDir, f), "Rewrite the migration using SQLite-compatible syntax: TEXT instead of JSONB, INTEGER PRIMARY KEY for auto-increment, datetime('now') instead of NOW(), no :: casts");
|
|
238
|
+
}
|
|
234
239
|
}
|
|
235
240
|
}
|
|
236
241
|
}
|
|
@@ -238,10 +243,13 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
238
243
|
// readdir failure — skip
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
|
-
|
|
242
|
-
const seedPath = join(root, 'db', 'sqlite-seed.sql');
|
|
246
|
+
const seedPath = join(root, 'db', dbEngine === 'postgres' ? 'postgres-seed.sql' : 'sqlite-seed.sql');
|
|
243
247
|
if (!existsSync(seedPath)) {
|
|
244
|
-
e('seed-sql-missing',
|
|
248
|
+
e('seed-sql-missing', dbEngine === 'postgres'
|
|
249
|
+
? 'db/postgres-seed.sql not found'
|
|
250
|
+
: 'db/sqlite-seed.sql not found', seedPath, dbEngine === 'postgres'
|
|
251
|
+
? 'Create db/postgres-seed.sql with idempotent seed data for local/test data'
|
|
252
|
+
: 'Create db/sqlite-seed.sql with idempotent INSERT OR IGNORE statements for demo/test data');
|
|
245
253
|
}
|
|
246
254
|
// audit table — info if not present (optional feature)
|
|
247
255
|
if (existsSync(migrationsDir)) {
|
|
@@ -328,8 +336,8 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
328
336
|
}
|
|
329
337
|
// ── packages/theme + packages/components ──────────────────────────────
|
|
330
338
|
const designDocUrl = 'https://pikkufabric.dev/docs/design';
|
|
331
|
-
if (!existsSync(join(root, 'packages', 'theme'))) {
|
|
332
|
-
info('theme-missing', 'packages/theme/ not found — Fabric design features require a theme package', join(root, 'packages', 'theme'), `Create packages/theme/ with your Mantine theme tokens. See ${designDocUrl}`);
|
|
339
|
+
if (!existsSync(join(root, 'packages', 'mantine-theme'))) {
|
|
340
|
+
info('theme-missing', 'packages/mantine-theme/ not found — Fabric design features require a theme package', join(root, 'packages', 'mantine-theme'), `Create packages/mantine-theme/ with your Mantine theme tokens. See ${designDocUrl}`);
|
|
333
341
|
}
|
|
334
342
|
if (!existsSync(join(root, 'packages', 'components'))) {
|
|
335
343
|
info('components-missing', 'packages/components/ not found — Fabric design features require a components package', join(root, 'packages', 'components'), `Create packages/components/ with your shared UI components. See ${designDocUrl}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
3
|
-
* Pins the project link (id, default api url) and declares deployable
|
|
4
|
-
* + production domain. Discovered by walking up from cwd until found.
|
|
2
|
+
* `pikkufabric.config.json` lives next to `pikku.config.json` in the project
|
|
3
|
+
* root. Pins the project link (id, default api url) and declares deployable
|
|
4
|
+
* apps + production domain. Discovered by walking up from cwd until found.
|
|
5
5
|
*/
|
|
6
6
|
export interface ProjectConfig {
|
|
7
7
|
projectId: string;
|
|
@@ -53,7 +53,7 @@ export interface ResolvedApiContext {
|
|
|
53
53
|
/**
|
|
54
54
|
* Stitch together the api-url + auth token from the standard sources:
|
|
55
55
|
* 1. explicit override (e.g. --api-url flag)
|
|
56
|
-
* 2.
|
|
56
|
+
* 2. pikkufabric.config.json apiUrl
|
|
57
57
|
* 3. FABRIC_API_URL env var
|
|
58
58
|
* 4. hardcoded default
|
|
59
59
|
*
|
|
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
const DEFAULT_API_URL = 'http://localhost:7103';
|
|
6
|
-
const projectConfigName = '
|
|
6
|
+
const projectConfigName = 'pikkufabric.config.json';
|
|
7
7
|
const authFilePath = join(homedir(), '.fabric', 'auth.json');
|
|
8
8
|
export async function findProjectConfig(startDir = process.cwd()) {
|
|
9
9
|
let dir = startDir;
|
|
@@ -41,7 +41,7 @@ export async function writeAuthFile(file) {
|
|
|
41
41
|
/**
|
|
42
42
|
* Stitch together the api-url + auth token from the standard sources:
|
|
43
43
|
* 1. explicit override (e.g. --api-url flag)
|
|
44
|
-
* 2.
|
|
44
|
+
* 2. pikkufabric.config.json apiUrl
|
|
45
45
|
* 3. FABRIC_API_URL env var
|
|
46
46
|
* 4. hardcoded default
|
|
47
47
|
*
|
|
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
|
|
4
4
|
import { join, extname, dirname } from 'node:path';
|
|
5
5
|
import { PIKKU_BETTER_AUTH } from '@pikku/better-auth';
|
|
6
|
+
import { LocalSecretService, LocalVariablesService } from '@pikku/core/services';
|
|
6
7
|
import { loadUserModule } from '../commands/load-user-project.js';
|
|
7
8
|
let cachedGetMigrations = null;
|
|
8
9
|
async function loadGetMigrations() {
|
|
@@ -87,24 +88,43 @@ async function loadAuthFactory(sourceFile) {
|
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
89
90
|
function schemaServicesStub(kysely, logger) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
91
|
+
const variables = new LocalVariablesService();
|
|
92
|
+
const secrets = new LocalSecretService(variables);
|
|
92
93
|
const base = {
|
|
93
94
|
kysely,
|
|
94
95
|
logger,
|
|
95
|
-
secrets
|
|
96
|
-
|
|
97
|
-
getSecrets: async (keys) => fromKeys(keys),
|
|
98
|
-
},
|
|
99
|
-
variables: {
|
|
100
|
-
getVariable: async () => dummy,
|
|
101
|
-
getVariables: async (keys) => fromKeys(keys),
|
|
102
|
-
},
|
|
96
|
+
secrets,
|
|
97
|
+
variables,
|
|
103
98
|
};
|
|
104
99
|
return new Proxy(base, {
|
|
105
100
|
get: (target, prop) => typeof prop === 'string' && prop in target ? target[prop] : undefined,
|
|
106
101
|
});
|
|
107
102
|
}
|
|
103
|
+
function findUserConfigFactoryFile(rootDir, srcDirectories) {
|
|
104
|
+
for (const srcDir of srcDirectories) {
|
|
105
|
+
for (const name of ['config.ts', 'config.js']) {
|
|
106
|
+
const candidate = join(rootDir, srcDir, name);
|
|
107
|
+
if (existsSync(candidate))
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const name of ['config.ts', 'config.js']) {
|
|
112
|
+
const candidate = join(rootDir, name);
|
|
113
|
+
if (existsSync(candidate))
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
async function loadAuthConfig(opts) {
|
|
119
|
+
const configFactoryFile = findUserConfigFactoryFile(opts.rootDir, opts.srcDirectories);
|
|
120
|
+
if (!configFactoryFile)
|
|
121
|
+
return undefined;
|
|
122
|
+
const configModule = await loadUserModule(configFactoryFile);
|
|
123
|
+
const userCreateConfig = configModule.createConfig;
|
|
124
|
+
if (typeof userCreateConfig !== 'function')
|
|
125
|
+
return undefined;
|
|
126
|
+
return userCreateConfig(new LocalVariablesService());
|
|
127
|
+
}
|
|
108
128
|
export async function loadAuthOptions(opts) {
|
|
109
129
|
const sourceFile = findAuthSourceFile(opts.rootDir, opts.srcDirectories);
|
|
110
130
|
if (!sourceFile)
|
|
@@ -112,7 +132,9 @@ export async function loadAuthOptions(opts) {
|
|
|
112
132
|
const factory = await loadAuthFactory(sourceFile);
|
|
113
133
|
if (!factory)
|
|
114
134
|
return null;
|
|
115
|
-
const
|
|
135
|
+
const services = schemaServicesStub(opts.kysely, opts.logger);
|
|
136
|
+
services.config = await loadAuthConfig(opts);
|
|
137
|
+
const instance = await factory(services);
|
|
116
138
|
const options = instance.options;
|
|
117
139
|
return options ?? null;
|
|
118
140
|
}
|
|
@@ -28,6 +28,12 @@ function snakeToPascal(name) {
|
|
|
28
28
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
29
29
|
.join('');
|
|
30
30
|
}
|
|
31
|
+
function tableToInterfaceName(name) {
|
|
32
|
+
return name
|
|
33
|
+
.split('.')
|
|
34
|
+
.map((part) => snakeToPascal(part))
|
|
35
|
+
.join('');
|
|
36
|
+
}
|
|
31
37
|
function snakeToCamel(name) {
|
|
32
38
|
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
33
39
|
}
|
|
@@ -110,9 +116,7 @@ function columnTypeExpression(col, annotation, classification) {
|
|
|
110
116
|
return wrap(nullable ? 'Uuid | null' : 'Uuid');
|
|
111
117
|
}
|
|
112
118
|
if (annotation?.tsType) {
|
|
113
|
-
const base = nullable
|
|
114
|
-
? `${annotation.tsType} | null`
|
|
115
|
-
: annotation.tsType;
|
|
119
|
+
const base = nullable ? `${annotation.tsType} | null` : annotation.tsType;
|
|
116
120
|
return wrap(base);
|
|
117
121
|
}
|
|
118
122
|
if (annotation?.kind === 'json') {
|
|
@@ -146,7 +150,7 @@ function bareTableName(name) {
|
|
|
146
150
|
return dot >= 0 ? name.slice(dot + 1) : name;
|
|
147
151
|
}
|
|
148
152
|
function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByName, formatHints, warnings) {
|
|
149
|
-
const ifaceName =
|
|
153
|
+
const ifaceName = tableToInterfaceName(table.name);
|
|
150
154
|
const bare = bareTableName(table.name);
|
|
151
155
|
const tableCols = explicitAnnotations[bare] ?? {};
|
|
152
156
|
const fields = table.columns
|
|
@@ -352,7 +356,7 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
352
356
|
const safe = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableKey)
|
|
353
357
|
? tableKey
|
|
354
358
|
: JSON.stringify(tableKey);
|
|
355
|
-
return ` ${safe}: ${
|
|
359
|
+
return ` ${safe}: ${tableToInterfaceName(t.name)}`;
|
|
356
360
|
})
|
|
357
361
|
.join('\n');
|
|
358
362
|
const schemaBody = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
2
|
import { type MigrateResult } from './db-migrator.js';
|
|
3
3
|
import { type CodegenResult } from './db-codegen.js';
|
|
4
4
|
import { type ZodCodegenResult } from './zod-codegen.js';
|
|
@@ -58,7 +58,7 @@ export interface DesiredAuthSchema {
|
|
|
58
58
|
tables: SchemaMap;
|
|
59
59
|
sql: string;
|
|
60
60
|
}
|
|
61
|
-
export declare function desiredAuthSchema(rootDir: string, srcDirectories: string[], logger: {
|
|
61
|
+
export declare function desiredAuthSchema(resolved: ResolvedDb, rootDir: string, srcDirectories: string[], logger: {
|
|
62
62
|
error: (msg: string) => void;
|
|
63
63
|
}): Promise<DesiredAuthSchema | null>;
|
|
64
64
|
export declare function introspectSchema(resolved: ResolvedDb): Promise<SchemaMap>;
|
|
@@ -3,6 +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
7
|
import { migrate } from './db-migrator.js';
|
|
7
8
|
import { loadAuthOptions, getAuthMigrations } from './better-auth-schema.js';
|
|
8
9
|
import { generateSchemaTypes } from './db-codegen.js';
|
|
@@ -319,14 +320,114 @@ function diffSchemas(desired, actual) {
|
|
|
319
320
|
}
|
|
320
321
|
return { missingTables, missingColumns };
|
|
321
322
|
}
|
|
322
|
-
|
|
323
|
+
function isPostgresAuthDatabase(options) {
|
|
324
|
+
return options.database?.type === 'postgres';
|
|
325
|
+
}
|
|
326
|
+
function createScratchPostgresSchemaName() {
|
|
327
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
328
|
+
return `pikku_auth_${Date.now().toString(36)}_${random}`;
|
|
329
|
+
}
|
|
330
|
+
async function postgresSchemaToMap(connectionString, schema) {
|
|
331
|
+
const { Client } = await import('pg');
|
|
332
|
+
const client = new Client({ connectionString });
|
|
333
|
+
await client.connect();
|
|
334
|
+
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;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
await client.end();
|
|
353
|
+
}
|
|
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
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
const admin = await pool.connect();
|
|
364
|
+
try {
|
|
365
|
+
await admin.query(`CREATE SCHEMA "${schema}"`);
|
|
366
|
+
await admin.query(`SET search_path TO "${schema}"`);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
admin.release();
|
|
370
|
+
}
|
|
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);
|
|
380
|
+
try {
|
|
381
|
+
const options = await loadAuthOptions({
|
|
382
|
+
rootDir,
|
|
383
|
+
srcDirectories,
|
|
384
|
+
kysely,
|
|
385
|
+
logger,
|
|
386
|
+
});
|
|
387
|
+
if (!options)
|
|
388
|
+
return null;
|
|
389
|
+
const { runMigrations, compileMigrations } = await getAuthMigrations(options);
|
|
390
|
+
await runMigrations();
|
|
391
|
+
const tables = await postgresSchemaToMap(resolved.connectionString, schema);
|
|
392
|
+
const sql = await compileMigrations();
|
|
393
|
+
return { tables, sql };
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
await kysely.destroy();
|
|
397
|
+
}
|
|
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
|
+
}
|
|
411
|
+
}
|
|
412
|
+
export async function desiredAuthSchema(resolved, rootDir, srcDirectories, logger) {
|
|
323
413
|
const runtime = await loadSqliteRuntime();
|
|
324
414
|
const db = runtime.open(':memory:');
|
|
325
415
|
try {
|
|
326
416
|
const kysely = createSqliteKysely({ db, camelCase: true });
|
|
327
|
-
const options = await loadAuthOptions({
|
|
417
|
+
const options = await loadAuthOptions({
|
|
418
|
+
rootDir,
|
|
419
|
+
srcDirectories,
|
|
420
|
+
kysely,
|
|
421
|
+
logger,
|
|
422
|
+
});
|
|
328
423
|
if (!options)
|
|
329
424
|
return null;
|
|
425
|
+
if (isPostgresAuthDatabase(options)) {
|
|
426
|
+
if (resolved.dialect !== 'postgres') {
|
|
427
|
+
throw new Error('Better Auth database.type is postgres, but the resolved app database is not postgres.');
|
|
428
|
+
}
|
|
429
|
+
return desiredPostgresAuthSchema(resolved, rootDir, srcDirectories, logger);
|
|
430
|
+
}
|
|
330
431
|
const { runMigrations, compileMigrations } = await getAuthMigrations(options);
|
|
331
432
|
await runMigrations();
|
|
332
433
|
const tables = await introspectorToMap(new SqliteIntrospector(db));
|
|
@@ -369,9 +470,14 @@ async function coveredSqliteSchema(migrationsDir) {
|
|
|
369
470
|
}
|
|
370
471
|
}
|
|
371
472
|
export async function computeAuthDrift(resolved, rootDir, srcDirectories, logger) {
|
|
372
|
-
const desired = await desiredAuthSchema(rootDir, srcDirectories, logger);
|
|
473
|
+
const desired = await desiredAuthSchema(resolved, rootDir, srcDirectories, logger);
|
|
373
474
|
if (!desired) {
|
|
374
|
-
return {
|
|
475
|
+
return {
|
|
476
|
+
hasAuth: false,
|
|
477
|
+
inSync: true,
|
|
478
|
+
missingTables: [],
|
|
479
|
+
missingColumns: [],
|
|
480
|
+
};
|
|
375
481
|
}
|
|
376
482
|
const actual = await introspectSchema(resolved);
|
|
377
483
|
const { missingTables, missingColumns } = diffSchemas(desired.tables, actual);
|
|
@@ -401,7 +507,7 @@ function nextMigrationFile(migrationsDir, label) {
|
|
|
401
507
|
export async function generateAuthMigration(resolved, rootDir, srcDirectories, logger) {
|
|
402
508
|
if (resolved.dialect !== 'sqlite')
|
|
403
509
|
return { status: 'unsupported-dialect' };
|
|
404
|
-
const desired = await desiredAuthSchema(rootDir, srcDirectories, logger);
|
|
510
|
+
const desired = await desiredAuthSchema(resolved, rootDir, srcDirectories, logger);
|
|
405
511
|
if (!desired)
|
|
406
512
|
return { status: 'no-auth' };
|
|
407
513
|
const covered = await coveredSqliteSchema(resolved.migrationsDir);
|