@pikku/cli 0.12.49 → 0.12.51
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-DybYeIfd.js → index-92DoVRHq.js} +142 -139
- 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-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.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 +70 -70
- 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 +3 -3
- 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.js +2 -1
- package/dist/src/deploy/provider-adapter.d.ts +13 -0
- package/dist/src/deploy/server-entry.d.ts +1 -1
- package/dist/src/deploy/server-entry.js +3 -1
- package/dist/src/fabric/functions/validate.function.js +6 -0
- package/dist/src/fabric/lib/config.js +1 -1
- package/dist/src/functions/commands/dev-ai-runner.d.ts +19 -0
- package/dist/src/functions/commands/dev-ai-runner.js +70 -0
- package/dist/src/functions/commands/dev.js +16 -0
- package/dist/src/functions/commands/tests-coverage.js +6 -0
- package/dist/src/functions/db/db-codegen.d.ts +9 -6
- package/dist/src/functions/db/db-codegen.js +62 -5
- package/dist/src/functions/db/db-introspector.d.ts +6 -0
- package/dist/src/functions/db/local-db.d.ts +1 -0
- package/dist/src/functions/db/local-db.js +3 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +8 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +27 -0
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/skills/pikku-paraglide/SKILL.md +117 -0
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.51') {
|
|
15
|
+
process.stderr.write(`\n Update available 0.12.51 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
|
|
16
16
|
}
|
|
17
17
|
} catch {}
|
|
18
18
|
}
|
|
@@ -188,7 +188,8 @@ export async function runBuildPipeline(options) {
|
|
|
188
188
|
await mkdir(unitDir, { recursive: true });
|
|
189
189
|
const ctx = getEntryContext(unitDir, pikkuDir, unit, inspectorState);
|
|
190
190
|
const source = unit.target === 'server'
|
|
191
|
-
? generateServerEntrySource(ctx)
|
|
191
|
+
? (provider.generateServerEntrySource?.(ctx) ??
|
|
192
|
+
generateServerEntrySource(ctx))
|
|
192
193
|
: provider.generateEntrySource(ctx);
|
|
193
194
|
await writeFile(entryPath, source, 'utf-8');
|
|
194
195
|
if (unit.target === 'server') {
|
|
@@ -59,6 +59,19 @@ export interface ProviderAdapter {
|
|
|
59
59
|
* Called once per unit.
|
|
60
60
|
*/
|
|
61
61
|
generateEntrySource(ctx: EntryGenerationContext): string;
|
|
62
|
+
/**
|
|
63
|
+
* Generate the entry file source for a `target: 'server'` (container) unit.
|
|
64
|
+
*
|
|
65
|
+
* Optional. When a provider implements it, the deploy pipeline uses it
|
|
66
|
+
* instead of the provider-agnostic `generateServerEntrySource`, so the
|
|
67
|
+
* provider can inject the SAME platform services (kysely, secrets, …) into
|
|
68
|
+
* the container that it injects into its serverless workers — sourcing the
|
|
69
|
+
* bindings from `process.env` instead of a runtime `env` object. When
|
|
70
|
+
* omitted, the pipeline falls back to the generic generator (no platform
|
|
71
|
+
* injection), which is correct for providers whose containers carry no
|
|
72
|
+
* platform services.
|
|
73
|
+
*/
|
|
74
|
+
generateServerEntrySource?(ctx: EntryGenerationContext): string;
|
|
62
75
|
/**
|
|
63
76
|
* Generate provider-specific config files per unit (e.g. wrangler.toml).
|
|
64
77
|
* Returns a map of filename → content to write into the unit directory.
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { EntryGenerationContext } from './provider-adapter.js';
|
|
14
14
|
export declare function generateServerEntrySource(ctx: EntryGenerationContext): string;
|
|
15
|
-
export declare const SERVER_DOCKERFILE = "# Generated by @pikku/cli \u2014 do not edit\nFROM node:
|
|
15
|
+
export declare const SERVER_DOCKERFILE = "# Generated by @pikku/cli \u2014 do not edit\n# Full image (not -slim): carries the build toolchain so externalised deps with\n# native addons can compile from source at `npm install` time.\nFROM node:26\n\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends tini ca-certificates \\\n && rm -rf /var/lib/apt/lists/*\n\nRUN groupadd -r pikku && useradd -r -g pikku -u 1001 pikku\n\nWORKDIR /app\n\n# Pikku bundles user code + first-party deps inline; only externalised deps\n# (typically empty for pure-JS apps) are listed in package.json. We ship no\n# lockfile because the bundle has already pinned everything via the\n# extracted exact-dependencies \u2014 `npm install --omit=dev` resolves the\n# leftover externals (if any) at build time.\nCOPY --chown=pikku:pikku package.json ./\nRUN npm install --omit=dev --no-audit --no-fund\n\nCOPY --chown=pikku:pikku bundle.js ./\n\nUSER pikku\nENV NODE_ENV=production\nENV PORT=8080\nEXPOSE 8080\n\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nCMD [\"node\", \"bundle.js\"]\n";
|
|
16
16
|
export declare const SERVER_DOCKERIGNORE = "node_modules\n.git\n*.log\nmetafile.json\nexact-dependencies.json\n";
|
|
@@ -45,7 +45,9 @@ export function generateServerEntrySource(ctx) {
|
|
|
45
45
|
return lines.join('\n');
|
|
46
46
|
}
|
|
47
47
|
export const SERVER_DOCKERFILE = `# Generated by @pikku/cli — do not edit
|
|
48
|
-
|
|
48
|
+
# Full image (not -slim): carries the build toolchain so externalised deps with
|
|
49
|
+
# native addons can compile from source at \`npm install\` time.
|
|
50
|
+
FROM node:26
|
|
49
51
|
|
|
50
52
|
RUN apt-get update \\
|
|
51
53
|
&& apt-get install -y --no-install-recommends tini ca-certificates \\
|
|
@@ -387,6 +387,12 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
387
387
|
if (!fnPkg?.dependencies?.['@ai-sdk/openai-compatible']) {
|
|
388
388
|
e('missing-ai-sdk-openai-compatible', 'Project declares agent units but @ai-sdk/openai-compatible is not in packages/functions dependencies', fnPkgPath, 'Run `yarn add @ai-sdk/openai-compatible` in packages/functions — must be in dependencies, not devDependencies');
|
|
389
389
|
}
|
|
390
|
+
// `ai` is a peer dep of @pikku/ai-vercel — not auto-installed. Without it
|
|
391
|
+
// `pikku dev` can't construct the agent runner and agents 503 with
|
|
392
|
+
// AIProviderNotConfiguredError.
|
|
393
|
+
if (!fnPkg?.dependencies?.['ai']) {
|
|
394
|
+
e('missing-ai-sdk-core', 'Project declares agent units but `ai` (the Vercel AI SDK) is not in packages/functions dependencies — it is a peer dependency of @pikku/ai-vercel and is not installed automatically', fnPkgPath, 'Run `yarn add ai` in packages/functions — must be in dependencies, not devDependencies');
|
|
395
|
+
}
|
|
390
396
|
}
|
|
391
397
|
// services.ts
|
|
392
398
|
const servicesPath = join(fnDir, 'src', 'services.ts');
|
|
@@ -2,7 +2,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
const DEFAULT_API_URL = '
|
|
5
|
+
const DEFAULT_API_URL = 'https://api.pikkufabric.com';
|
|
6
6
|
const projectConfigName = 'pikkufabric.config.json';
|
|
7
7
|
const authFilePath = join(homedir(), '.fabric', 'auth.json');
|
|
8
8
|
export async function findProjectConfig(startDir = process.cwd()) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Logger, VariablesService } from '@pikku/core/services';
|
|
2
|
+
import type { AIAgentRunnerService } from '@pikku/core/services';
|
|
3
|
+
/**
|
|
4
|
+
* Build the AI agent runner for `pikku dev` from env.
|
|
5
|
+
*
|
|
6
|
+
* Deployed agent units get their runner wired by the bundler; the dev server
|
|
7
|
+
* has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
|
|
8
|
+
* construct one here. When an OpenAI-compatible base URL + key are present
|
|
9
|
+
* (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
|
|
10
|
+
* vars are also honored) we point a single openai-compatible provider at it and
|
|
11
|
+
* register it under every common provider prefix. Returns undefined when no AI
|
|
12
|
+
* env is configured (agents stay disabled, with the clear downstream error) or
|
|
13
|
+
* when the AI SDK packages aren't installed in the project.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createDevAIAgentRunner({ logger, projectRoot, variables, }: {
|
|
16
|
+
logger: Logger;
|
|
17
|
+
projectRoot: string;
|
|
18
|
+
variables: VariablesService;
|
|
19
|
+
}): Promise<AIAgentRunnerService | undefined>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
// Provider prefixes a fabric/proxy baseURL fronts. Models are written as
|
|
5
|
+
// `openai/gpt-4o-mini`, `openai/deepseek-v4-flash`, etc. — the runner splits on
|
|
6
|
+
// the first `/`, so the prefix only selects the provider entry and the bare
|
|
7
|
+
// model name is forwarded to the OpenAI-compatible proxy, which routes it.
|
|
8
|
+
const PROXY_PROVIDER_NAMES = [
|
|
9
|
+
'openai',
|
|
10
|
+
'anthropic',
|
|
11
|
+
'google',
|
|
12
|
+
'gemini',
|
|
13
|
+
'deepseek',
|
|
14
|
+
'xai',
|
|
15
|
+
'litellm',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Build the AI agent runner for `pikku dev` from env.
|
|
19
|
+
*
|
|
20
|
+
* Deployed agent units get their runner wired by the bundler; the dev server
|
|
21
|
+
* has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
|
|
22
|
+
* construct one here. When an OpenAI-compatible base URL + key are present
|
|
23
|
+
* (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
|
|
24
|
+
* vars are also honored) we point a single openai-compatible provider at it and
|
|
25
|
+
* register it under every common provider prefix. Returns undefined when no AI
|
|
26
|
+
* env is configured (agents stay disabled, with the clear downstream error) or
|
|
27
|
+
* when the AI SDK packages aren't installed in the project.
|
|
28
|
+
*/
|
|
29
|
+
export async function createDevAIAgentRunner({ logger, projectRoot, variables, }) {
|
|
30
|
+
// Pair the URL with its matching key — coalescing each var independently could
|
|
31
|
+
// combine an OPENAI_BASE_URL with a LITELLM_API_KEY (or vice versa) and
|
|
32
|
+
// misroute or 401 every call. Take a complete OpenAI pair first, else LiteLLM.
|
|
33
|
+
const openAIBaseURL = await variables.get('OPENAI_BASE_URL');
|
|
34
|
+
const openAIApiKey = await variables.get('OPENAI_API_KEY');
|
|
35
|
+
const liteLLMBaseURL = await variables.get('LITELLM_PROXY_URL');
|
|
36
|
+
const liteLLMApiKey = await variables.get('LITELLM_API_KEY');
|
|
37
|
+
const [baseURL, apiKey] = openAIBaseURL && openAIApiKey
|
|
38
|
+
? [openAIBaseURL, openAIApiKey]
|
|
39
|
+
: liteLLMBaseURL && liteLLMApiKey
|
|
40
|
+
? [liteLLMBaseURL, liteLLMApiKey]
|
|
41
|
+
: [undefined, undefined];
|
|
42
|
+
if (!baseURL || !apiKey) {
|
|
43
|
+
logger.debug('pikku dev: no AI provider env (OPENAI_BASE_URL/OPENAI_API_KEY or LITELLM_PROXY_URL/LITELLM_API_KEY) — AI agents disabled');
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// Resolve from the project's node_modules — the AI SDK packages are the
|
|
47
|
+
// project's deps, not the CLI's, so they share the project's `ai` version.
|
|
48
|
+
const require = createRequire(pathToFileURL(join(projectRoot, 'package.json')).href);
|
|
49
|
+
let VercelAIAgentRunner;
|
|
50
|
+
let createOpenAICompatible;
|
|
51
|
+
try {
|
|
52
|
+
;
|
|
53
|
+
({ VercelAIAgentRunner } = await import(pathToFileURL(require.resolve('@pikku/ai-vercel')).href));
|
|
54
|
+
({ createOpenAICompatible } = await import(pathToFileURL(require.resolve('@ai-sdk/openai-compatible')).href));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.warn(`pikku dev: AI provider env is set but the AI SDK packages could not be loaded (install @pikku/ai-vercel, @ai-sdk/openai-compatible, and ai) — AI agents disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const buildProviders = (key) => {
|
|
61
|
+
const provider = createOpenAICompatible({
|
|
62
|
+
name: 'pikku-dev',
|
|
63
|
+
baseURL,
|
|
64
|
+
apiKey: key,
|
|
65
|
+
});
|
|
66
|
+
return Object.fromEntries(PROXY_PROVIDER_NAMES.map((name) => [name, provider]));
|
|
67
|
+
};
|
|
68
|
+
logger.info(`pikku dev: AI agent runner wired to ${baseURL}`);
|
|
69
|
+
return new VercelAIAgentRunner(buildProviders(apiKey), buildProviders);
|
|
70
|
+
}
|
|
@@ -15,6 +15,7 @@ import { WebSocketServer } from 'ws';
|
|
|
15
15
|
import { InMemorySchedulerService } from '@pikku/schedule';
|
|
16
16
|
import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
|
|
17
17
|
import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
|
|
18
|
+
import { createDevAIAgentRunner } from './dev-ai-runner.js';
|
|
18
19
|
export const dev = pikkuSessionlessFunc({
|
|
19
20
|
remote: true,
|
|
20
21
|
func: async ({ logger, config, getInspectorState, variables }, { port, watch, hmr }, { rpc }) => {
|
|
@@ -153,8 +154,23 @@ export const dev = pikkuSessionlessFunc({
|
|
|
153
154
|
// can read runs in dev without projects having to wire their own backing
|
|
154
155
|
// store.
|
|
155
156
|
const devLogger = new ConsoleLogger();
|
|
157
|
+
// Deployed agent units get their runner from the bundler; the dev server
|
|
158
|
+
// has no equivalent, so construct one from env or agents 503 with
|
|
159
|
+
// AIProviderNotConfiguredError. The template forwards injected services
|
|
160
|
+
// (`...existingServices`) so this reaches getSingletonServices().
|
|
161
|
+
// Only when the project declares agents — otherwise the runner's
|
|
162
|
+
// missing-SDK warning fires spuriously for projects with global AI env.
|
|
163
|
+
const hasAgents = Object.keys(inspectorState.agents.agentsMeta).length > 0;
|
|
164
|
+
const aiAgentRunner = hasAgents
|
|
165
|
+
? await createDevAIAgentRunner({
|
|
166
|
+
logger,
|
|
167
|
+
projectRoot: config.rootDir,
|
|
168
|
+
variables,
|
|
169
|
+
})
|
|
170
|
+
: undefined;
|
|
156
171
|
const inMemoryServices = {
|
|
157
172
|
logger: devLogger,
|
|
173
|
+
...(aiAgentRunner ? { aiAgentRunner } : {}),
|
|
158
174
|
emailService: new LocalEmailService(),
|
|
159
175
|
metaService: new LocalMetaService(pikkuDir),
|
|
160
176
|
schedulerService,
|
|
@@ -165,6 +165,12 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
|
|
|
165
165
|
'--require',
|
|
166
166
|
'tests/tests/support/**/*.ts',
|
|
167
167
|
'tests/tests/features/**/*.feature',
|
|
168
|
+
'--format',
|
|
169
|
+
'progress',
|
|
170
|
+
// Persist the HTML report so the console can parse scenarios back out
|
|
171
|
+
// of it (readAllMeta attaches them per function).
|
|
172
|
+
'--format',
|
|
173
|
+
'html:tests/tests/reports/cucumber-report.html',
|
|
168
174
|
], { cwd: functionsDir, stdio: 'inherit', env: spawnEnv });
|
|
169
175
|
if (res.status !== 0) {
|
|
170
176
|
logger.error(`Test run failed (exit ${res.status})`);
|
|
@@ -7,6 +7,13 @@ export interface CodegenOptions {
|
|
|
7
7
|
manifestFile?: string;
|
|
8
8
|
classificationMapFile?: string;
|
|
9
9
|
schemaJsonFile?: string;
|
|
10
|
+
/**
|
|
11
|
+
* When set, emit a standalone module of bare enum unions (one
|
|
12
|
+
* `export type <Table><Column>` per enum column) — independent of the wrapped
|
|
13
|
+
* DB interface, so callers import a clean union instead of unwrapping
|
|
14
|
+
* `ColumnType<Private<…>>`.
|
|
15
|
+
*/
|
|
16
|
+
enumsFile?: string;
|
|
10
17
|
camelCase?: boolean;
|
|
11
18
|
rootDir?: string;
|
|
12
19
|
/** DB dialect — drives real-type-aware date typing. Defaults to 'sqlite'. */
|
|
@@ -17,10 +24,12 @@ export interface CodegenResult {
|
|
|
17
24
|
coercionFile: string;
|
|
18
25
|
manifestFile?: string;
|
|
19
26
|
classificationMapFile?: string;
|
|
27
|
+
enumsFile?: string;
|
|
20
28
|
written: boolean;
|
|
21
29
|
coercionWritten: boolean;
|
|
22
30
|
manifestWritten: boolean;
|
|
23
31
|
classificationMapWritten: boolean;
|
|
32
|
+
enumsWritten: boolean;
|
|
24
33
|
tables: string[];
|
|
25
34
|
/** Non-fatal codegen warnings (e.g. name looks like a date but unannotated). */
|
|
26
35
|
warnings: string[];
|
|
@@ -31,11 +40,5 @@ export interface CodegenResult {
|
|
|
31
40
|
*/
|
|
32
41
|
zodFormats: Record<string, Record<string, ZodFormat>>;
|
|
33
42
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Introspect `introspector` and emit:
|
|
36
|
-
* - `schema.d.ts` Kysely DB type with classification brands
|
|
37
|
-
* - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
|
|
38
|
-
* - `classification.gen.ts` Data-classification manifest (when manifestFile set)
|
|
39
|
-
*/
|
|
40
43
|
export declare function generateSchemaTypes(introspector: DbIntrospector, options: CodegenOptions): Promise<CodegenResult>;
|
|
41
44
|
export {};
|
|
@@ -177,11 +177,13 @@ function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByNam
|
|
|
177
177
|
`is ${col.type} (${real}). If intentional, set its kind in db/annotations.ts.`);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
180
|
+
// Enum columns are typed as a union of string literals — only when no
|
|
181
|
+
// explicit `tsType`/`kind` overrides it. This reuses the `tsType` plumbing,
|
|
182
|
+
// so it flows through both the public and classified branches. Values come
|
|
183
|
+
// from a Postgres enum (`type` is 'USER-DEFINED', real values via `udtName`)
|
|
184
|
+
// or, on SQLite, from a `CHECK (col IN (…))` constraint the introspector
|
|
185
|
+
// parsed onto `col.enumValues`.
|
|
186
|
+
const enumValues = col.enumValues ?? (col.udtName ? enumByName.get(col.udtName) : undefined);
|
|
185
187
|
const enumUnion = enumValues && enumValues.length > 0
|
|
186
188
|
? enumValues.map((v) => `'${escapeTsString(v)}'`).join(' | ')
|
|
187
189
|
: null;
|
|
@@ -322,6 +324,41 @@ function emitClassificationMap(tables) {
|
|
|
322
324
|
* - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
|
|
323
325
|
* - `classification.gen.ts` Data-classification manifest (when manifestFile set)
|
|
324
326
|
*/
|
|
327
|
+
/**
|
|
328
|
+
* Bare string-literal enum types, independent of the wrapped DB interface. One
|
|
329
|
+
* `export type <Table><Column>` per enum column — Postgres native enums (values
|
|
330
|
+
* via `udtName`/`enumByName`) and SQLite `CHECK (col IN (…))` (values on
|
|
331
|
+
* `col.enumValues`) alike. Lets app code and i18n reconciliation import a clean
|
|
332
|
+
* union instead of unwrapping `ColumnType<Private<…>>` from the DB interface.
|
|
333
|
+
*/
|
|
334
|
+
function emitEnumsModule(tables, enumByName) {
|
|
335
|
+
const pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
const lines = [];
|
|
338
|
+
for (const t of tables) {
|
|
339
|
+
const tablePart = pascal(snakeToCamel(bareTableName(t.name)));
|
|
340
|
+
for (const col of t.columns) {
|
|
341
|
+
const values = col.enumValues ?? (col.udtName ? enumByName.get(col.udtName) : undefined);
|
|
342
|
+
if (!values || values.length === 0)
|
|
343
|
+
continue;
|
|
344
|
+
const name = `${tablePart}${pascal(snakeToCamel(col.name))}`;
|
|
345
|
+
if (seen.has(name))
|
|
346
|
+
continue;
|
|
347
|
+
seen.add(name);
|
|
348
|
+
const union = values.map((v) => `'${escapeTsString(v)}'`).join(' | ');
|
|
349
|
+
lines.push(`export type ${name} = ${union}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return [
|
|
353
|
+
`// Generated by @pikku/cli — do not edit by hand.`,
|
|
354
|
+
`// Run \`pikku db migrate\` to refresh.`,
|
|
355
|
+
`//`,
|
|
356
|
+
`// Bare enum unions from Postgres enums and SQLite CHECK (col IN (…)) constraints.`,
|
|
357
|
+
``,
|
|
358
|
+
...(lines.length ? lines : ['export {}']),
|
|
359
|
+
``,
|
|
360
|
+
].join('\n');
|
|
361
|
+
}
|
|
325
362
|
export async function generateSchemaTypes(introspector, options) {
|
|
326
363
|
const camelCase = options.camelCase ?? true;
|
|
327
364
|
const dialect = options.dialect ?? 'sqlite';
|
|
@@ -456,6 +493,10 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
456
493
|
schemaJsonBody =
|
|
457
494
|
JSON.stringify({ tables: jsonTables, enums }, null, 2) + '\n';
|
|
458
495
|
}
|
|
496
|
+
// ── enums.gen.ts ─────────────────────────────────────────────────────────────
|
|
497
|
+
const enumsBody = options.enumsFile
|
|
498
|
+
? emitEnumsModule(tables, enumByName)
|
|
499
|
+
: null;
|
|
459
500
|
// ── write files ───────────────────────────────────────────────────────────────
|
|
460
501
|
let existingSchema = null;
|
|
461
502
|
let existingCoercion = null;
|
|
@@ -498,12 +539,22 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
498
539
|
/* ok */
|
|
499
540
|
}
|
|
500
541
|
}
|
|
542
|
+
let existingEnums = null;
|
|
543
|
+
if (options.enumsFile) {
|
|
544
|
+
try {
|
|
545
|
+
existingEnums = readFileSync(options.enumsFile, 'utf8');
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
/* ok */
|
|
549
|
+
}
|
|
550
|
+
}
|
|
501
551
|
const schemaChanged = existingSchema !== schemaBody;
|
|
502
552
|
const coercionChanged = existingCoercion !== coercionBody;
|
|
503
553
|
const manifestChanged = manifestBody !== null && existingManifest !== manifestBody;
|
|
504
554
|
const classificationMapChanged = classificationMapBody !== null &&
|
|
505
555
|
existingClassificationMap !== classificationMapBody;
|
|
506
556
|
const schemaJsonChanged = schemaJsonBody !== null && existingSchemaJson !== schemaJsonBody;
|
|
557
|
+
const enumsChanged = enumsBody !== null && existingEnums !== enumsBody;
|
|
507
558
|
if (schemaChanged) {
|
|
508
559
|
mkdirSync(dirname(options.outFile), { recursive: true });
|
|
509
560
|
writeFileSync(options.outFile, schemaBody, 'utf8');
|
|
@@ -526,15 +577,21 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
526
577
|
mkdirSync(dirname(options.schemaJsonFile), { recursive: true });
|
|
527
578
|
writeFileSync(options.schemaJsonFile, schemaJsonBody, 'utf8');
|
|
528
579
|
}
|
|
580
|
+
if (enumsChanged && options.enumsFile && enumsBody) {
|
|
581
|
+
mkdirSync(dirname(options.enumsFile), { recursive: true });
|
|
582
|
+
writeFileSync(options.enumsFile, enumsBody, 'utf8');
|
|
583
|
+
}
|
|
529
584
|
return {
|
|
530
585
|
outFile: options.outFile,
|
|
531
586
|
coercionFile: options.coercionFile,
|
|
532
587
|
manifestFile: options.manifestFile,
|
|
533
588
|
classificationMapFile: options.classificationMapFile,
|
|
589
|
+
enumsFile: options.enumsFile,
|
|
534
590
|
written: schemaChanged,
|
|
535
591
|
coercionWritten: coercionChanged,
|
|
536
592
|
manifestWritten: manifestChanged,
|
|
537
593
|
classificationMapWritten: classificationMapChanged,
|
|
594
|
+
enumsWritten: enumsChanged,
|
|
538
595
|
tables: tables.map((t) => t.name),
|
|
539
596
|
warnings,
|
|
540
597
|
zodFormats,
|
|
@@ -13,6 +13,12 @@ export interface ColumnInfo {
|
|
|
13
13
|
* name used to resolve its values. Undefined on SQLite (no native enums).
|
|
14
14
|
*/
|
|
15
15
|
udtName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* String-literal enum values for this column, when known. On SQLite (no native
|
|
18
|
+
* enums) these come from a `CHECK (col IN ('a','b',…))` constraint — the CHECK
|
|
19
|
+
* *is* the enum definition. Typed as a union, mirroring a Postgres enum column.
|
|
20
|
+
*/
|
|
21
|
+
enumValues?: string[];
|
|
16
22
|
}
|
|
17
23
|
export interface ForeignKeyInfo {
|
|
18
24
|
column: string;
|
|
@@ -47,6 +47,7 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
|
|
|
47
47
|
manifestFile: join(outDir, 'db', 'classification.gen.ts'),
|
|
48
48
|
classificationMapFile: join(outDir, 'db', 'classification-map.gen.d.ts'),
|
|
49
49
|
schemaJsonFile: join(outDir, 'db', 'pikku-db-schema.gen.json'),
|
|
50
|
+
enumsFile: join(outDir, 'db', 'enums.gen.ts'),
|
|
50
51
|
classificationsFile: join(rootDir, 'db', 'annotations.ts'),
|
|
51
52
|
// Compiled sidecar lives beside the authored annotations.ts in db/ — this is
|
|
52
53
|
// where both consumers read it: the codegen's loadAnnotations() and the
|
|
@@ -182,6 +183,7 @@ export async function migrateAndCodegen(resolved) {
|
|
|
182
183
|
manifestFile: resolved.manifestFile,
|
|
183
184
|
classificationMapFile: resolved.classificationMapFile,
|
|
184
185
|
schemaJsonFile: resolved.schemaJsonFile,
|
|
186
|
+
enumsFile: resolved.enumsFile,
|
|
185
187
|
camelCase: resolved.camelCase,
|
|
186
188
|
rootDir: resolved.rootDir,
|
|
187
189
|
dialect: 'sqlite',
|
|
@@ -204,6 +206,7 @@ export async function migrateAndCodegen(resolved) {
|
|
|
204
206
|
manifestFile: resolved.manifestFile,
|
|
205
207
|
classificationMapFile: resolved.classificationMapFile,
|
|
206
208
|
schemaJsonFile: resolved.schemaJsonFile,
|
|
209
|
+
enumsFile: resolved.enumsFile,
|
|
207
210
|
camelCase: resolved.camelCase,
|
|
208
211
|
rootDir: resolved.rootDir,
|
|
209
212
|
dialect: 'postgres',
|
|
@@ -5,6 +5,14 @@ export declare class SqliteIntrospector implements DbIntrospector {
|
|
|
5
5
|
constructor(db: SyncSqliteDatabase);
|
|
6
6
|
listTables(): Promise<string[]>;
|
|
7
7
|
getColumns(table: string): Promise<ColumnInfo[]>;
|
|
8
|
+
/**
|
|
9
|
+
* SQLite has no native enums, but a `CHECK (col IN ('a','b',…))` constraint is
|
|
10
|
+
* an enum by another name. Parse the table's stored `CREATE TABLE` DDL and map
|
|
11
|
+
* each constrained column to its allowed values so codegen can type it as a
|
|
12
|
+
* union. Only the positive `col IN (…)` form is recognised (the convention);
|
|
13
|
+
* `NOT IN`, ranges, and boolean expressions are left as plain `string`.
|
|
14
|
+
*/
|
|
15
|
+
private parseCheckEnums;
|
|
8
16
|
getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
|
|
9
17
|
listEnums(): Promise<EnumInfo[]>;
|
|
10
18
|
close(): Promise<void>;
|
|
@@ -18,6 +18,7 @@ export class SqliteIntrospector {
|
|
|
18
18
|
const rows = this.db
|
|
19
19
|
.prepare(`PRAGMA table_xinfo(${escaped})`)
|
|
20
20
|
.all();
|
|
21
|
+
const enumsByColumn = this.parseCheckEnums(table);
|
|
21
22
|
return rows
|
|
22
23
|
.filter((c) => c.hidden !== 1)
|
|
23
24
|
.map((c) => ({
|
|
@@ -27,8 +28,34 @@ export class SqliteIntrospector {
|
|
|
27
28
|
pk: c.pk > 0,
|
|
28
29
|
defaultValue: c.dflt_value != null ? String(c.dflt_value) : null,
|
|
29
30
|
generated: c.hidden === 2 || c.hidden === 3,
|
|
31
|
+
enumValues: enumsByColumn.get(c.name),
|
|
30
32
|
}));
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* SQLite has no native enums, but a `CHECK (col IN ('a','b',…))` constraint is
|
|
36
|
+
* an enum by another name. Parse the table's stored `CREATE TABLE` DDL and map
|
|
37
|
+
* each constrained column to its allowed values so codegen can type it as a
|
|
38
|
+
* union. Only the positive `col IN (…)` form is recognised (the convention);
|
|
39
|
+
* `NOT IN`, ranges, and boolean expressions are left as plain `string`.
|
|
40
|
+
*/
|
|
41
|
+
parseCheckEnums(table) {
|
|
42
|
+
const out = new Map();
|
|
43
|
+
const row = this.db
|
|
44
|
+
.prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
45
|
+
.get(table);
|
|
46
|
+
const ddl = row?.sql;
|
|
47
|
+
if (!ddl)
|
|
48
|
+
return out;
|
|
49
|
+
const checkIn = /CHECK\s*\(\s*"?(\w+)"?\s+IN\s*\(([^)]*)\)/gi;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = checkIn.exec(ddl))) {
|
|
52
|
+
const column = m[1];
|
|
53
|
+
const values = [...m[2].matchAll(/'((?:[^']|'')*)'/g)].map((q) => q[1].replace(/''/g, "'"));
|
|
54
|
+
if (values.length > 0)
|
|
55
|
+
out.set(column, values);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
32
59
|
async getForeignKeys(table) {
|
|
33
60
|
const escaped = `"${table.replace(/"/g, '""')}"`;
|
|
34
61
|
const rows = this.db
|