@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.
Files changed (80) hide show
  1. package/console-app/assets/{index-M7009WaW.js → index-NmNeixvK.js} +140 -140
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +3 -0
  15. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  19. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  20. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  21. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  22. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  23. package/dist/.pikku/function/pikku-functions-meta.gen.json +6 -6
  24. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  25. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  26. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  27. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  29. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  32. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  34. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  36. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  37. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  44. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  46. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  47. package/dist/.pikku/schemas/register.gen.js +1 -1
  48. package/dist/.pikku/schemas/schemas/PikkuNewAddonInput.schema.json +1 -1
  49. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  50. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  51. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  52. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  53. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  54. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  55. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  56. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  57. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  58. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  59. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +5 -4
  60. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  61. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  62. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  63. package/dist/bin/pikku-bin.mjs +2 -2
  64. package/dist/src/cli.wiring.js +3 -0
  65. package/dist/src/functions/commands/db-migrate.js +3 -0
  66. package/dist/src/functions/commands/db-reset.js +3 -0
  67. package/dist/src/functions/commands/new-addon.d.ts +3 -0
  68. package/dist/src/functions/commands/new-addon.js +53 -4
  69. package/dist/src/functions/db/db-codegen.d.ts +7 -2
  70. package/dist/src/functions/db/db-codegen.js +32 -14
  71. package/dist/src/functions/db/db-introspector.d.ts +13 -0
  72. package/dist/src/functions/db/local-db.js +1 -2
  73. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +3 -2
  74. package/dist/src/functions/db/postgres/postgres-introspector.js +109 -7
  75. package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +2 -0
  76. package/dist/src/functions/db/sqlite/sqlite-introspector.js +16 -0
  77. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +2 -1
  78. package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
  79. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +16 -4
  80. 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.credential && flags.credential !== 'oauth2') {
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
- token: z.string().describe('${displayName} bearer token'),
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
- /** Non-fatal codegen warnings (e.g. name looks like a date but unannotated). */
35
- warnings: string[];
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(`Column "${bare}.${col.name}" is named like a ${suggested} but its DB type ` +
177
- `is ${col.type} (${real}). If intentional, set its kind in db/annotations.ts.`);
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(`Column "${bare}.${col.name}" is ${col.type} and will be typed as ` +
205
- `\`${resolved}\` — JSON/JSONB columns need a concrete TypeScript type. ` +
206
- `In db/annotations.ts add: "${col.name}": { kind: 'json', tsType: 'YourType' }.`);
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(`Column "${bare}.${col.name}": format '${ann.format}' ignored — its ` +
221
- `resolved type is ${base}, not string. Remove the conflicting kind/tsType.`);
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 tables = await Promise.all(tableNames.map(async (name) => ({
402
+ const columnsByTable = await introspector.getAllColumns();
403
+ const tables = tableNames.map((name) => ({
383
404
  name,
384
- columns: await introspector.getColumns(name),
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 fkResults = await Promise.all(tableNames.map((name) => introspector.getForeignKeys(name)));
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.listTables()) {
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 === 'public'
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 === 'public'
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
- await writeFileInDir(logger, workflowTypesFile, serializeWorkflowTypes(functionTypesImportPath, rpcMapImportPath, workflowMapImportPath));
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
- : Record<string, unknown>
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
- : string
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<string, keyof FlattenedRPCMap & string>
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.76",
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.15",
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.6",
35
- "@pikku/inspector": "^0.12.38",
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.5",
39
- "@pikku/openapi-parser": "^0.12.11",
40
- "@pikku/schedule": "^0.12.3",
41
- "@pikku/ws": "^0.12.3",
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.57"
62
+ "@pikku/core": "^0.12.58"
63
63
  },
64
64
  "devDependencies": {
65
- "@pikku/core": "^0.12.57",
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": ">=22"
78
+ "node": ">=24"
79
79
  },
80
80
  "exports": {
81
81
  ".": "./dist/bin/pikku.js"