@pikku/cli 0.12.76 → 0.12.77
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-M7009WaW.js → index-NmNeixvK.js} +140 -140
- 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-meta.gen.json +3 -0
- 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 +6 -6
- 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/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 +1 -1
- package/dist/.pikku/schemas/schemas/PikkuNewAddonInput.schema.json +1 -1
- 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 +5 -4
- 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/cli.wiring.js +3 -0
- package/dist/src/functions/commands/db-migrate.js +3 -0
- package/dist/src/functions/commands/db-reset.js +3 -0
- package/dist/src/functions/commands/new-addon.d.ts +3 -0
- package/dist/src/functions/commands/new-addon.js +53 -4
- package/dist/src/functions/db/db-codegen.d.ts +7 -2
- package/dist/src/functions/db/db-codegen.js +32 -14
- package/dist/src/functions/db/db-introspector.d.ts +13 -0
- package/dist/src/functions/db/local-db.js +1 -2
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +3 -2
- package/dist/src/functions/db/postgres/postgres-introspector.js +109 -7
- package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +2 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +16 -0
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +2 -1
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +16 -4
- package/package.json +11 -11
|
@@ -10,6 +10,7 @@ export declare const pikkuNewAddon: import("#pikku").PikkuFunctionConfig<{
|
|
|
10
10
|
credential?: string;
|
|
11
11
|
test?: boolean;
|
|
12
12
|
openapi?: string;
|
|
13
|
+
authConfig?: string;
|
|
13
14
|
mcp?: boolean;
|
|
14
15
|
camelCase?: boolean;
|
|
15
16
|
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
@@ -24,6 +25,7 @@ export declare const pikkuNewAddon: import("#pikku").PikkuFunctionConfig<{
|
|
|
24
25
|
credential?: string;
|
|
25
26
|
test?: boolean;
|
|
26
27
|
openapi?: string;
|
|
28
|
+
authConfig?: string;
|
|
27
29
|
mcp?: boolean;
|
|
28
30
|
camelCase?: boolean;
|
|
29
31
|
}, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
|
|
@@ -38,6 +40,7 @@ export declare const pikkuNewAddon: import("#pikku").PikkuFunctionConfig<{
|
|
|
38
40
|
credential?: string;
|
|
39
41
|
test?: boolean;
|
|
40
42
|
openapi?: string;
|
|
43
|
+
authConfig?: string;
|
|
41
44
|
mcp?: boolean;
|
|
42
45
|
camelCase?: boolean;
|
|
43
46
|
}, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
|
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { mkdir, writeFile } from 'fs/promises';
|
|
4
4
|
import { createEmptyManifest, saveManifest, } from '../../utils/contract-versions.js';
|
|
5
5
|
import { pikkuSessionlessFunc } from '#pikku';
|
|
6
|
-
import { parseOpenAPISpec, computeContractHash, generateAddonFromOpenAPI, } from '@pikku/openapi-parser';
|
|
6
|
+
import { parseOpenAPISpec, computeContractHash, generateAddonFromOpenAPI, loadAuthConfig, } from '@pikku/openapi-parser';
|
|
7
7
|
function toCamelCase(str) {
|
|
8
8
|
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
9
9
|
}
|
|
@@ -174,7 +174,34 @@ ${description}
|
|
|
174
174
|
// export { ${camelName}Operation } from './functions/operation.function.js'
|
|
175
175
|
`;
|
|
176
176
|
// src/services.ts
|
|
177
|
-
if (flags.
|
|
177
|
+
if (flags.delegated) {
|
|
178
|
+
// Delegated login: the credential is the upstream token captured at
|
|
179
|
+
// sign-in; an expired token is the re-auth signal (UnauthorizedError).
|
|
180
|
+
files['src/services.ts'] =
|
|
181
|
+
`import { UnauthorizedError } from '@pikku/core/errors'
|
|
182
|
+
import { ${pascalName}Service } from './${name}-api.service.js'
|
|
183
|
+
import { pikkuAddonWireServices } from '#pikku'
|
|
184
|
+
|
|
185
|
+
export const createWireServices = pikkuAddonWireServices(
|
|
186
|
+
async ({ variables }, wire) => {
|
|
187
|
+
if (!wire.getCredential) {
|
|
188
|
+
throw new Error('Credential resolution is not available in this runtime')
|
|
189
|
+
}
|
|
190
|
+
const cred = await wire.getCredential<{ token: string; expiresAt?: number }>('${camelName}')
|
|
191
|
+
if (!cred?.token) {
|
|
192
|
+
throw new UnauthorizedError('No ${displayName} session — sign in again')
|
|
193
|
+
}
|
|
194
|
+
if (cred.expiresAt && cred.expiresAt * 1000 < Date.now()) {
|
|
195
|
+
throw new UnauthorizedError('${displayName} session expired — sign in again')
|
|
196
|
+
}
|
|
197
|
+
const ${camelName} = new ${pascalName}Service(cred, variables)
|
|
198
|
+
|
|
199
|
+
return { ${camelName} }
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
else if (flags.credential && flags.credential !== 'oauth2') {
|
|
178
205
|
// Per-user credential: use createWireServices with wire.getCredential()
|
|
179
206
|
const credField = flags.credential === 'bearer' ? 'token' : 'apiKey';
|
|
180
207
|
files['src/services.ts'] =
|
|
@@ -508,11 +535,16 @@ wireCredential({
|
|
|
508
535
|
`;
|
|
509
536
|
}
|
|
510
537
|
else if (flags.credential === 'bearer') {
|
|
538
|
+
const credentialFields = flags.delegated
|
|
539
|
+
? ` token: z.string().describe('${displayName} session token captured at delegated sign-in'),
|
|
540
|
+
expiresAt: z.number().optional().describe('Token expiry (epoch seconds)'),
|
|
541
|
+
tenantId: z.string().optional().describe('Upstream tenant id'),`
|
|
542
|
+
: ` token: z.string().describe('${displayName} bearer token'),`;
|
|
511
543
|
files[`src/${name}.credential.ts`] = `import { z } from 'zod'
|
|
512
544
|
import { wireCredential } from '@pikku/core/credential'
|
|
513
545
|
|
|
514
546
|
export const ${camelName}CredentialSchema = z.object({
|
|
515
|
-
|
|
547
|
+
${credentialFields}
|
|
516
548
|
})
|
|
517
549
|
|
|
518
550
|
wireCredential({
|
|
@@ -766,7 +798,7 @@ async function writeFiles(baseDir, files) {
|
|
|
766
798
|
return written;
|
|
767
799
|
}
|
|
768
800
|
export const pikkuNewAddon = pikkuSessionlessFunc({
|
|
769
|
-
func: async ({ logger, config }, { name, displayName, description, category = 'General', dir, secret = false, variable = false, oauth = false, credential, test = true, openapi, mcp = false, camelCase = false, }) => {
|
|
801
|
+
func: async ({ logger, config }, { name, displayName, description, category = 'General', dir, secret = false, variable = false, oauth = false, credential, test = true, openapi, authConfig, mcp = false, camelCase = false, }) => {
|
|
770
802
|
name = sanitizeAddonName(name);
|
|
771
803
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
772
804
|
logger.error(`Invalid addon name "${name}": must start with a lowercase letter and contain only lowercase alphanumerics, hyphens, and underscores`);
|
|
@@ -791,6 +823,20 @@ export const pikkuNewAddon = pikkuSessionlessFunc({
|
|
|
791
823
|
description: resolvedDescription,
|
|
792
824
|
category,
|
|
793
825
|
};
|
|
826
|
+
// Load the auth config (custom auth header / delegated login overrides)
|
|
827
|
+
let loadedAuthConfig;
|
|
828
|
+
if (authConfig) {
|
|
829
|
+
loadedAuthConfig = await loadAuthConfig(authConfig);
|
|
830
|
+
if (loadedAuthConfig.delegated) {
|
|
831
|
+
// Delegated login stores the upstream token per-user — bearer is the
|
|
832
|
+
// only coherent credential mode.
|
|
833
|
+
if (credential && credential !== 'bearer') {
|
|
834
|
+
logger.error(`--auth-config with delegated login requires --credential bearer (got "${credential}")`);
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
credential = 'bearer';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
794
840
|
// Validate credential type if provided
|
|
795
841
|
const credentialType = credential;
|
|
796
842
|
if (credentialType &&
|
|
@@ -805,6 +851,7 @@ export const pikkuNewAddon = pikkuSessionlessFunc({
|
|
|
805
851
|
variable,
|
|
806
852
|
oauth: effectiveOAuth,
|
|
807
853
|
credential: credentialType,
|
|
854
|
+
delegated: Boolean(loadedAuthConfig?.delegated),
|
|
808
855
|
});
|
|
809
856
|
// If openapi spec provided, generate typed files and merge over scaffold
|
|
810
857
|
if (openapi) {
|
|
@@ -815,6 +862,7 @@ export const pikkuNewAddon = pikkuSessionlessFunc({
|
|
|
815
862
|
credential: credentialType,
|
|
816
863
|
mcp,
|
|
817
864
|
camelCase,
|
|
865
|
+
authConfig: loadedAuthConfig,
|
|
818
866
|
});
|
|
819
867
|
Object.assign(addonFiles, openapiFiles);
|
|
820
868
|
// Inject openapi metadata into pikku.config.json
|
|
@@ -826,6 +874,7 @@ export const pikkuNewAddon = pikkuSessionlessFunc({
|
|
|
826
874
|
version: spec.info.version,
|
|
827
875
|
hash: computeContractHash(spec),
|
|
828
876
|
...(camelCase ? { camelCase: true } : {}),
|
|
877
|
+
...(loadedAuthConfig ? { authConfig: true } : {}),
|
|
829
878
|
};
|
|
830
879
|
addonFiles['pikku.config.json'] = JSON.stringify(config, null, 2);
|
|
831
880
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DbIntrospector } from './db-introspector.js';
|
|
2
2
|
import { type ZodFormat } from './zod-codegen.js';
|
|
3
|
+
import { type CodedDiagnostic } from '@pikku/inspector';
|
|
3
4
|
type Dialect = 'sqlite' | 'postgres';
|
|
4
5
|
export interface CodegenOptions {
|
|
5
6
|
outFile: string;
|
|
@@ -31,8 +32,12 @@ export interface CodegenResult {
|
|
|
31
32
|
classificationMapWritten: boolean;
|
|
32
33
|
enumsWritten: boolean;
|
|
33
34
|
tables: string[];
|
|
34
|
-
/**
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Non-fatal codegen diagnostics (e.g. name looks like a date but unannotated,
|
|
37
|
+
* or a JSON column with no concrete `tsType`). The caller surfaces these via
|
|
38
|
+
* `logger.diagnostic(...)`, so `pikku db --fail-on-warn` can gate on them.
|
|
39
|
+
*/
|
|
40
|
+
warnings: CodedDiagnostic[];
|
|
36
41
|
/**
|
|
37
42
|
* Per-interface, per-field zod `format` overrides for the zod codegen. Keyed
|
|
38
43
|
* by interface name (PascalCase) and field name (camelCase), matching the
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { loadAnnotations, nameSuggestsKind, } from './annotation-parser.js';
|
|
4
4
|
import { ZOD_FORMATS } from './zod-codegen.js';
|
|
5
|
+
import { ErrorCode } from '@pikku/inspector';
|
|
5
6
|
/**
|
|
6
7
|
* The column kind implied by the *real* DB type, dialect-aware. Postgres has
|
|
7
8
|
* native temporal/boolean types so we can trust them; SQLite stores dates as
|
|
@@ -13,6 +14,9 @@ function realKind(dialect, sqlType) {
|
|
|
13
14
|
if (dialect !== 'postgres')
|
|
14
15
|
return null;
|
|
15
16
|
const u = sqlType.toUpperCase();
|
|
17
|
+
// Array types carry no scalar kind — an array of timestamps is not a `date`.
|
|
18
|
+
if (u.endsWith('[]'))
|
|
19
|
+
return null;
|
|
16
20
|
if (u.includes('TIMESTAMP') || u === 'DATE')
|
|
17
21
|
return 'date';
|
|
18
22
|
if (u === 'BOOLEAN' || u === 'BOOL')
|
|
@@ -44,6 +48,10 @@ function escapeTsString(value) {
|
|
|
44
48
|
// ─── Type mapping ─────────────────────────────────────────────────────────────
|
|
45
49
|
function mapType(sqlType) {
|
|
46
50
|
const upper = sqlType.toUpperCase();
|
|
51
|
+
// Preserve array-ness — map the element type and re-apply the `[]` suffix
|
|
52
|
+
// rather than letting the element's substring match flatten it to a scalar.
|
|
53
|
+
if (upper.endsWith('[]'))
|
|
54
|
+
return `${mapType(sqlType.slice(0, -2))}[]`;
|
|
47
55
|
if (upper.includes('INT'))
|
|
48
56
|
return 'number';
|
|
49
57
|
if (upper.includes('CHAR') ||
|
|
@@ -173,8 +181,12 @@ function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByNam
|
|
|
173
181
|
if (!ann?.kind && !ann?.tsType) {
|
|
174
182
|
const suggested = nameSuggestsKind(col.name);
|
|
175
183
|
if (suggested && real && suggested !== real) {
|
|
176
|
-
warnings.push(
|
|
177
|
-
|
|
184
|
+
warnings.push({
|
|
185
|
+
severity: 'warn',
|
|
186
|
+
code: ErrorCode.DB_COLUMN_NAME_TYPE_CONTRADICTION,
|
|
187
|
+
message: `Column "${bare}.${col.name}" is named like a ${suggested} but its DB type ` +
|
|
188
|
+
`is ${col.type} (${real}). If intentional, set its kind in db/annotations.ts.`,
|
|
189
|
+
});
|
|
178
190
|
}
|
|
179
191
|
}
|
|
180
192
|
// Enum columns are typed as a union of string literals — only when no
|
|
@@ -201,9 +213,13 @@ function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByNam
|
|
|
201
213
|
if (isJsonColumn) {
|
|
202
214
|
const resolved = selectBase(typeAnn, col);
|
|
203
215
|
if (resolved === 'unknown' || resolved === 'any') {
|
|
204
|
-
warnings.push(
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
warnings.push({
|
|
217
|
+
severity: 'warn',
|
|
218
|
+
code: ErrorCode.DB_JSON_COLUMN_UNTYPED,
|
|
219
|
+
message: `Column "${bare}.${col.name}" is ${col.type} and will be typed as ` +
|
|
220
|
+
`\`${resolved}\` — JSON/JSONB columns need a concrete TypeScript type. ` +
|
|
221
|
+
`In db/annotations.ts add: "${col.name}": { kind: 'json', tsType: 'YourType' }.`,
|
|
222
|
+
});
|
|
207
223
|
}
|
|
208
224
|
}
|
|
209
225
|
// A `format` validator refines the zod schema only and keeps the TS type
|
|
@@ -217,8 +233,12 @@ function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByNam
|
|
|
217
233
|
(formatHints[ifaceName] ??= {})[fieldName] = ann.format;
|
|
218
234
|
}
|
|
219
235
|
else {
|
|
220
|
-
warnings.push(
|
|
221
|
-
|
|
236
|
+
warnings.push({
|
|
237
|
+
severity: 'warn',
|
|
238
|
+
code: ErrorCode.DB_FORMAT_ON_NON_STRING,
|
|
239
|
+
message: `Column "${bare}.${col.name}": format '${ann.format}' ignored — its ` +
|
|
240
|
+
`resolved type is ${base}, not string. Remove the conflicting kind/tsType.`,
|
|
241
|
+
});
|
|
222
242
|
}
|
|
223
243
|
}
|
|
224
244
|
const classification = ann?.classification ?? 'private';
|
|
@@ -379,10 +399,11 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
379
399
|
const camelCase = options.camelCase ?? true;
|
|
380
400
|
const dialect = options.dialect ?? 'sqlite';
|
|
381
401
|
const tableNames = await introspector.listTables();
|
|
382
|
-
const
|
|
402
|
+
const columnsByTable = await introspector.getAllColumns();
|
|
403
|
+
const tables = tableNames.map((name) => ({
|
|
383
404
|
name,
|
|
384
|
-
columns:
|
|
385
|
-
}))
|
|
405
|
+
columns: columnsByTable.get(name) ?? [],
|
|
406
|
+
}));
|
|
386
407
|
const explicitAnnotations = options.rootDir
|
|
387
408
|
? loadAnnotations(options.rootDir)
|
|
388
409
|
: {};
|
|
@@ -401,8 +422,6 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
401
422
|
const interfaces = tables
|
|
402
423
|
.map((t) => emitInterface(t, camelCase, explicitAnnotations, dialect, enumByName, zodFormats, warnings))
|
|
403
424
|
.join('\n\n');
|
|
404
|
-
for (const w of warnings)
|
|
405
|
-
console.warn(`[pikku db] ${w}`);
|
|
406
425
|
const dbEntries = tables
|
|
407
426
|
.map((t) => {
|
|
408
427
|
const tableKey = camelCase ? snakeToCamel(t.name) : t.name;
|
|
@@ -484,8 +503,7 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
484
503
|
// ── pikku-db-schema.gen.json ─────────────────────────────────────────────────
|
|
485
504
|
let schemaJsonBody = null;
|
|
486
505
|
if (options.schemaJsonFile) {
|
|
487
|
-
const
|
|
488
|
-
const fkMap = new Map(tableNames.map((name, i) => [name, fkResults[i]]));
|
|
506
|
+
const fkMap = await introspector.getAllForeignKeys();
|
|
489
507
|
const jsonTables = tables.map((t) => ({
|
|
490
508
|
name: t.name,
|
|
491
509
|
columns: t.columns.map((c) => {
|
|
@@ -34,6 +34,19 @@ export interface DbIntrospector {
|
|
|
34
34
|
listTables(): Promise<string[]>;
|
|
35
35
|
getColumns(table: string): Promise<ColumnInfo[]>;
|
|
36
36
|
getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Columns for every table in one sweep, keyed by the same table name that
|
|
39
|
+
* {@link listTables} returns (schema-qualified for non-`public` tables). On
|
|
40
|
+
* Postgres this is a single set-based query — introspecting a large schema
|
|
41
|
+
* must not fan out one round-trip per table, which both scales O(tables) and
|
|
42
|
+
* overlaps queries on a single connection.
|
|
43
|
+
*/
|
|
44
|
+
getAllColumns(): Promise<Map<string, ColumnInfo[]>>;
|
|
45
|
+
/**
|
|
46
|
+
* Foreign keys for every table in one sweep, keyed like {@link getAllColumns}.
|
|
47
|
+
* Tables with no foreign keys may be omitted from the map.
|
|
48
|
+
*/
|
|
49
|
+
getAllForeignKeys(): Promise<Map<string, ForeignKeyInfo[]>>;
|
|
37
50
|
listEnums(): Promise<EnumInfo[]>;
|
|
38
51
|
close(): Promise<void>;
|
|
39
52
|
}
|
|
@@ -435,8 +435,7 @@ export async function createKysely(resolved) {
|
|
|
435
435
|
}
|
|
436
436
|
async function introspectorToMap(intro) {
|
|
437
437
|
const map = new Map();
|
|
438
|
-
for (const table of await intro.
|
|
439
|
-
const cols = await intro.getColumns(table);
|
|
438
|
+
for (const [table, cols] of await intro.getAllColumns()) {
|
|
440
439
|
map.set(table, new Set(cols.map((c) => c.name)));
|
|
441
440
|
}
|
|
442
441
|
return map;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbIntrospector, ColumnInfo, ForeignKeyInfo, EnumInfo } from '../db-introspector.js';
|
|
2
|
-
interface QueryClient {
|
|
2
|
+
export interface QueryClient {
|
|
3
3
|
query<T = unknown>(sql: string, params?: unknown[]): Promise<{
|
|
4
4
|
rows: T[];
|
|
5
5
|
}>;
|
|
@@ -13,7 +13,8 @@ export declare class PostgresIntrospector implements DbIntrospector {
|
|
|
13
13
|
listTables(): Promise<string[]>;
|
|
14
14
|
getColumns(table: string): Promise<ColumnInfo[]>;
|
|
15
15
|
getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
|
|
16
|
+
getAllColumns(): Promise<Map<string, ColumnInfo[]>>;
|
|
17
|
+
getAllForeignKeys(): Promise<Map<string, ForeignKeyInfo[]>>;
|
|
16
18
|
listEnums(): Promise<EnumInfo[]>;
|
|
17
19
|
close(): Promise<void>;
|
|
18
20
|
}
|
|
19
|
-
export {};
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
+
/** Table display name matching `listTables` (schema-qualified unless `public`). */
|
|
3
|
+
function tableKey(schema, table) {
|
|
4
|
+
return schema === 'public' ? table : `${schema}.${table}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the SQL type string for a column, preserving array-ness. Postgres
|
|
8
|
+
* reports every array column with `data_type = 'ARRAY'`; the element type lives
|
|
9
|
+
* in `udt_name` as the internal array type name (`_text`, `_int4`, `_uuid`, …).
|
|
10
|
+
* We strip the leading underscore and re-form it as `<element>[]` so downstream
|
|
11
|
+
* type mapping keeps the array instead of flattening to the scalar element.
|
|
12
|
+
*/
|
|
13
|
+
function resolveColumnType(dataType, udtName) {
|
|
14
|
+
if (dataType === 'ARRAY') {
|
|
15
|
+
const element = udtName.startsWith('_') ? udtName.slice(1) : udtName;
|
|
16
|
+
return `${element}[]`;
|
|
17
|
+
}
|
|
18
|
+
return dataType;
|
|
19
|
+
}
|
|
2
20
|
export class PostgresIntrospector {
|
|
3
21
|
client;
|
|
4
22
|
ownsClient;
|
|
@@ -25,9 +43,7 @@ export class PostgresIntrospector {
|
|
|
25
43
|
AND table_schema NOT LIKE 'pg_temp_%'
|
|
26
44
|
AND table_type = 'BASE TABLE'
|
|
27
45
|
ORDER BY table_schema, table_name`);
|
|
28
|
-
return result.rows.map((r) => r.table_schema
|
|
29
|
-
? r.table_name
|
|
30
|
-
: `${r.table_schema}.${r.table_name}`);
|
|
46
|
+
return result.rows.map((r) => tableKey(r.table_schema, r.table_name));
|
|
31
47
|
}
|
|
32
48
|
async getColumns(table) {
|
|
33
49
|
const dotIdx = table.indexOf('.');
|
|
@@ -57,7 +73,7 @@ export class PostgresIntrospector {
|
|
|
57
73
|
ORDER BY c.ordinal_position`, [tableName, schema]);
|
|
58
74
|
return result.rows.map((r) => ({
|
|
59
75
|
name: r.column_name,
|
|
60
|
-
type: r.data_type,
|
|
76
|
+
type: resolveColumnType(r.data_type, r.udt_name),
|
|
61
77
|
udtName: r.udt_name,
|
|
62
78
|
notNull: r.is_nullable === 'NO',
|
|
63
79
|
pk: Boolean(r.is_pk),
|
|
@@ -87,12 +103,98 @@ export class PostgresIntrospector {
|
|
|
87
103
|
ORDER BY kcu.ordinal_position`, [tableName, schema]);
|
|
88
104
|
return result.rows.map((r) => ({
|
|
89
105
|
column: r.column_name,
|
|
90
|
-
foreignTable: r.foreign_table_schema
|
|
91
|
-
? r.foreign_table_name
|
|
92
|
-
: `${r.foreign_table_schema}.${r.foreign_table_name}`,
|
|
106
|
+
foreignTable: tableKey(r.foreign_table_schema, r.foreign_table_name),
|
|
93
107
|
foreignColumn: r.foreign_column_name,
|
|
94
108
|
}));
|
|
95
109
|
}
|
|
110
|
+
async getAllColumns() {
|
|
111
|
+
const result = await this.client.query(`WITH pk_cols AS (
|
|
112
|
+
SELECT tc.table_schema, tc.table_name, kcu.column_name
|
|
113
|
+
FROM information_schema.table_constraints tc
|
|
114
|
+
JOIN information_schema.key_column_usage kcu
|
|
115
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
116
|
+
AND tc.table_schema = kcu.table_schema
|
|
117
|
+
AND tc.table_name = kcu.table_name
|
|
118
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
119
|
+
)
|
|
120
|
+
SELECT
|
|
121
|
+
c.table_schema,
|
|
122
|
+
c.table_name,
|
|
123
|
+
c.column_name,
|
|
124
|
+
c.data_type,
|
|
125
|
+
c.udt_name,
|
|
126
|
+
c.is_nullable,
|
|
127
|
+
c.column_default,
|
|
128
|
+
c.is_generated,
|
|
129
|
+
(pk.column_name IS NOT NULL) AS is_pk
|
|
130
|
+
FROM information_schema.columns c
|
|
131
|
+
JOIN information_schema.tables t
|
|
132
|
+
ON t.table_schema = c.table_schema
|
|
133
|
+
AND t.table_name = c.table_name
|
|
134
|
+
AND t.table_type = 'BASE TABLE'
|
|
135
|
+
LEFT JOIN pk_cols pk
|
|
136
|
+
ON pk.table_schema = c.table_schema
|
|
137
|
+
AND pk.table_name = c.table_name
|
|
138
|
+
AND pk.column_name = c.column_name
|
|
139
|
+
WHERE c.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
140
|
+
AND c.table_schema NOT LIKE 'pg_temp_%'
|
|
141
|
+
ORDER BY c.table_schema, c.table_name, c.ordinal_position`);
|
|
142
|
+
const byTable = new Map();
|
|
143
|
+
for (const r of result.rows) {
|
|
144
|
+
const key = tableKey(r.table_schema, r.table_name);
|
|
145
|
+
let cols = byTable.get(key);
|
|
146
|
+
if (!cols) {
|
|
147
|
+
cols = [];
|
|
148
|
+
byTable.set(key, cols);
|
|
149
|
+
}
|
|
150
|
+
cols.push({
|
|
151
|
+
name: r.column_name,
|
|
152
|
+
type: resolveColumnType(r.data_type, r.udt_name),
|
|
153
|
+
udtName: r.udt_name,
|
|
154
|
+
notNull: r.is_nullable === 'NO',
|
|
155
|
+
pk: Boolean(r.is_pk),
|
|
156
|
+
defaultValue: r.column_default,
|
|
157
|
+
generated: r.is_generated === 'ALWAYS',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return byTable;
|
|
161
|
+
}
|
|
162
|
+
async getAllForeignKeys() {
|
|
163
|
+
const result = await this.client.query(`SELECT kcu.table_schema AS owner_schema,
|
|
164
|
+
kcu.table_name AS owner_table,
|
|
165
|
+
kcu.column_name,
|
|
166
|
+
kcu2.table_schema AS foreign_table_schema,
|
|
167
|
+
kcu2.table_name AS foreign_table_name,
|
|
168
|
+
kcu2.column_name AS foreign_column_name
|
|
169
|
+
FROM information_schema.referential_constraints rc
|
|
170
|
+
JOIN information_schema.key_column_usage kcu
|
|
171
|
+
ON kcu.constraint_catalog = rc.constraint_catalog
|
|
172
|
+
AND kcu.constraint_schema = rc.constraint_schema
|
|
173
|
+
AND kcu.constraint_name = rc.constraint_name
|
|
174
|
+
JOIN information_schema.key_column_usage kcu2
|
|
175
|
+
ON kcu2.constraint_catalog = rc.unique_constraint_catalog
|
|
176
|
+
AND kcu2.constraint_schema = rc.unique_constraint_schema
|
|
177
|
+
AND kcu2.constraint_name = rc.unique_constraint_name
|
|
178
|
+
AND kcu2.ordinal_position = kcu.ordinal_position
|
|
179
|
+
WHERE kcu.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
180
|
+
AND kcu.table_schema NOT LIKE 'pg_temp_%'
|
|
181
|
+
ORDER BY kcu.table_schema, kcu.table_name, kcu.ordinal_position`);
|
|
182
|
+
const byTable = new Map();
|
|
183
|
+
for (const r of result.rows) {
|
|
184
|
+
const key = tableKey(r.owner_schema, r.owner_table);
|
|
185
|
+
let fks = byTable.get(key);
|
|
186
|
+
if (!fks) {
|
|
187
|
+
fks = [];
|
|
188
|
+
byTable.set(key, fks);
|
|
189
|
+
}
|
|
190
|
+
fks.push({
|
|
191
|
+
column: r.column_name,
|
|
192
|
+
foreignTable: tableKey(r.foreign_table_schema, r.foreign_table_name),
|
|
193
|
+
foreignColumn: r.foreign_column_name,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return byTable;
|
|
197
|
+
}
|
|
96
198
|
async listEnums() {
|
|
97
199
|
const result = await this.client.query(`SELECT t.typname AS enum_name,
|
|
98
200
|
t.typnamespace::regnamespace::text AS schema_name,
|
|
@@ -14,6 +14,8 @@ export declare class SqliteIntrospector implements DbIntrospector {
|
|
|
14
14
|
*/
|
|
15
15
|
private parseCheckEnums;
|
|
16
16
|
getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
|
|
17
|
+
getAllColumns(): Promise<Map<string, ColumnInfo[]>>;
|
|
18
|
+
getAllForeignKeys(): Promise<Map<string, ForeignKeyInfo[]>>;
|
|
17
19
|
listEnums(): Promise<EnumInfo[]>;
|
|
18
20
|
close(): Promise<void>;
|
|
19
21
|
}
|
|
@@ -67,6 +67,22 @@ export class SqliteIntrospector {
|
|
|
67
67
|
foreignColumn: r.to,
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
|
+
async getAllColumns() {
|
|
71
|
+
const byTable = new Map();
|
|
72
|
+
for (const table of await this.listTables()) {
|
|
73
|
+
byTable.set(table, await this.getColumns(table));
|
|
74
|
+
}
|
|
75
|
+
return byTable;
|
|
76
|
+
}
|
|
77
|
+
async getAllForeignKeys() {
|
|
78
|
+
const byTable = new Map();
|
|
79
|
+
for (const table of await this.listTables()) {
|
|
80
|
+
const fks = await this.getForeignKeys(table);
|
|
81
|
+
if (fks.length > 0)
|
|
82
|
+
byTable.set(table, fks);
|
|
83
|
+
}
|
|
84
|
+
return byTable;
|
|
85
|
+
}
|
|
70
86
|
async listEnums() {
|
|
71
87
|
return [];
|
|
72
88
|
}
|
|
@@ -62,7 +62,8 @@ export const pikkuWorkflow = pikkuSessionlessFunc({
|
|
|
62
62
|
const functionTypesImportPath = getFileImportRelativePath(workflowTypesFile, functionTypesFile, packageMappings);
|
|
63
63
|
const rpcMapImportPath = getFileImportRelativePath(workflowTypesFile, config.rpcInternalMapDeclarationFile, packageMappings);
|
|
64
64
|
const workflowMapImportPath = getFileImportRelativePath(workflowTypesFile, workflowMapDeclarationFile, packageMappings);
|
|
65
|
-
|
|
65
|
+
const agentMapImportPath = getFileImportRelativePath(workflowTypesFile, config.agentMapDeclarationFile, packageMappings);
|
|
66
|
+
await writeFileInDir(logger, workflowTypesFile, serializeWorkflowTypes(functionTypesImportPath, rpcMapImportPath, workflowMapImportPath, agentMapImportPath));
|
|
66
67
|
const scenarioActors = config.scenarios?.actors;
|
|
67
68
|
if (scenarioActors && Object.keys(scenarioActors).length > 0) {
|
|
68
69
|
const agentMapImportPath = getFileImportRelativePath(config.scenarioActorsFile, config.agentMapDeclarationFile, packageMappings);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const serializeWorkflowTypes: (functionTypesImportPath: string, rpcMapImportPath: string, workflowMapImportPath: string) => string;
|
|
1
|
+
export declare const serializeWorkflowTypes: (functionTypesImportPath: string, rpcMapImportPath: string, workflowMapImportPath: string, agentMapImportPath: string) => string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const serializeWorkflowTypes = (functionTypesImportPath, rpcMapImportPath, workflowMapImportPath) => {
|
|
1
|
+
export const serializeWorkflowTypes = (functionTypesImportPath, rpcMapImportPath, workflowMapImportPath, agentMapImportPath) => {
|
|
2
2
|
return `import { WorkflowCancelledException } from '@pikku/core/workflow'
|
|
3
3
|
import { template } from '@pikku/core/workflow'
|
|
4
4
|
import {
|
|
@@ -12,6 +12,7 @@ export { WorkflowCancelledException }
|
|
|
12
12
|
import type { PikkuFunctionSessionless, PikkuFunctionConfig } from '${functionTypesImportPath}'
|
|
13
13
|
import type { FlattenedRPCMap } from '${rpcMapImportPath}'
|
|
14
14
|
import type { FlattenedWorkflowMap } from '${workflowMapImportPath}'
|
|
15
|
+
import type { AgentMap as FlattenedAgentMap } from '${agentMapImportPath}'
|
|
15
16
|
|
|
16
17
|
export { template }
|
|
17
18
|
|
|
@@ -144,13 +145,19 @@ type InputWithRefs<T> = {
|
|
|
144
145
|
type NodeInputType<FuncMap extends Record<string, string>, K extends keyof FuncMap> =
|
|
145
146
|
FuncMap[K] extends keyof FlattenedRPCMap
|
|
146
147
|
? InputWithRefs<FlattenedRPCMap[FuncMap[K]]['input']>
|
|
147
|
-
:
|
|
148
|
+
: FuncMap[K] extends keyof FlattenedWorkflowMap
|
|
149
|
+
? InputWithRefs<FlattenedWorkflowMap[FuncMap[K]]['input']>
|
|
150
|
+
: Record<string, unknown>
|
|
148
151
|
|
|
149
152
|
type NodeOutputKeys<FuncMap extends Record<string, string>, N extends string> =
|
|
150
153
|
N extends keyof FuncMap
|
|
151
154
|
? FuncMap[N] extends keyof FlattenedRPCMap
|
|
152
155
|
? keyof FlattenedRPCMap[FuncMap[N]]['output'] & string
|
|
153
|
-
:
|
|
156
|
+
: FuncMap[N] extends keyof FlattenedWorkflowMap
|
|
157
|
+
? keyof FlattenedWorkflowMap[FuncMap[N]]['output'] & string
|
|
158
|
+
: FuncMap[N] extends keyof FlattenedAgentMap
|
|
159
|
+
? keyof FlattenedAgentMap[FuncMap[N]]['output'] & string
|
|
160
|
+
: string
|
|
154
161
|
: string
|
|
155
162
|
|
|
156
163
|
type RefFunction<FuncMap extends Record<string, string>> = {
|
|
@@ -177,7 +184,12 @@ type GraphNodeConfigMap<FuncMap extends Record<string, string>> = {
|
|
|
177
184
|
type NextConfig<NodeIds extends string> = NodeIds | NodeIds[] | { if: string; then: NodeIds; else?: NodeIds }
|
|
178
185
|
|
|
179
186
|
export function pikkuWorkflowGraph<
|
|
180
|
-
const FuncMap extends Record<
|
|
187
|
+
const FuncMap extends Record<
|
|
188
|
+
string,
|
|
189
|
+
| (keyof FlattenedRPCMap & string)
|
|
190
|
+
| (keyof FlattenedWorkflowMap & string)
|
|
191
|
+
| (keyof FlattenedAgentMap & string)
|
|
192
|
+
>
|
|
181
193
|
>(
|
|
182
194
|
config: PikkuWorkflowGraphConfig<FuncMap, GraphNodeConfigMap<FuncMap>>
|
|
183
195
|
): PikkuWorkflowGraphResult {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.77",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"imports": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"@electric-sql/pglite": "^0.5.1",
|
|
29
29
|
"@jridgewell/trace-mapping": "^0.3.25",
|
|
30
30
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
31
|
-
"@pikku/better-auth": "^0.12.
|
|
31
|
+
"@pikku/better-auth": "^0.12.17",
|
|
32
32
|
"@pikku/bun-server": "^0.12.4",
|
|
33
33
|
"@pikku/deploy-cloudflare": "^0.12.8",
|
|
34
|
-
"@pikku/fetch": "^0.12.
|
|
35
|
-
"@pikku/inspector": "^0.12.
|
|
34
|
+
"@pikku/fetch": "^0.12.7",
|
|
35
|
+
"@pikku/inspector": "^0.12.39",
|
|
36
36
|
"@pikku/kysely": "^0.13.0",
|
|
37
37
|
"@pikku/kysely-node-sqlite": "^0.12.3",
|
|
38
|
-
"@pikku/node-http-server": "^0.12.
|
|
39
|
-
"@pikku/openapi-parser": "^0.12.
|
|
40
|
-
"@pikku/schedule": "^0.12.
|
|
41
|
-
"@pikku/ws": "^0.12.
|
|
38
|
+
"@pikku/node-http-server": "^0.12.6",
|
|
39
|
+
"@pikku/openapi-parser": "^0.12.13",
|
|
40
|
+
"@pikku/schedule": "^0.12.4",
|
|
41
|
+
"@pikku/ws": "^0.12.4",
|
|
42
42
|
"@types/cookie": "^1.0.0",
|
|
43
43
|
"@types/json-schema": "^7.0.15",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"zod-to-ts": "^2.1.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@pikku/core": "^0.12.
|
|
62
|
+
"@pikku/core": "^0.12.58"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@pikku/core": "^0.12.
|
|
65
|
+
"@pikku/core": "^0.12.58",
|
|
66
66
|
"@types/istanbul-lib-instrument": "^1.7.7",
|
|
67
67
|
"@types/node": "^24.11.0",
|
|
68
68
|
"@types/pg": "^8.11.0",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"cli.schema.json"
|
|
76
76
|
],
|
|
77
77
|
"engines": {
|
|
78
|
-
"node": ">=
|
|
78
|
+
"node": ">=24"
|
|
79
79
|
},
|
|
80
80
|
"exports": {
|
|
81
81
|
".": "./dist/bin/pikku.js"
|