@pikku/cli 0.12.33 → 0.12.35

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 (72) hide show
  1. package/console-app/assets/index-BOM3RFeu.js +233 -0
  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-types.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  14. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  15. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  16. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  17. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  18. package/dist/.pikku/function/pikku-functions-meta.gen.json +24 -24
  19. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  20. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  21. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  22. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  23. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  25. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  26. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  27. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  28. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  29. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  30. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  31. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-types.gen.js +1 -1
  34. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  35. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  36. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  38. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  39. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  40. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +2 -2
  41. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  42. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  43. package/dist/.pikku/schemas/register.gen.js +3 -3
  44. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  45. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  46. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  47. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  48. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  49. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  50. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  51. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  52. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  53. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  54. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  55. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  56. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  57. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  58. package/dist/bin/pikku-bin.mjs +2 -2
  59. package/dist/src/functions/db/annotation-parser.d.ts +27 -16
  60. package/dist/src/functions/db/annotation-parser.js +50 -110
  61. package/dist/src/functions/db/coercion-plugin.d.ts +1 -1
  62. package/dist/src/functions/db/coercion-plugin.js +4 -0
  63. package/dist/src/functions/db/db-codegen.d.ts +13 -1
  64. package/dist/src/functions/db/db-codegen.js +149 -34
  65. package/dist/src/functions/db/db-introspector.d.ts +6 -0
  66. package/dist/src/functions/db/local-db.js +96 -78
  67. package/dist/src/functions/db/postgres/postgres-introspector.js +2 -0
  68. package/dist/src/functions/db/zod-codegen.d.ts +38 -0
  69. package/dist/src/functions/db/zod-codegen.js +219 -32
  70. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  71. package/package.json +3 -3
  72. package/console-app/assets/index-DsW0T00Z.js +0 -233
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, } from 'node:fs';
2
2
  import { resolve, isAbsolute, relative, dirname, join } from 'node:path';
3
- import { execSync } from 'node:child_process';
4
3
  import { createRequire } from 'node:module';
5
- import { fileURLToPath } from 'node:url';
4
+ import { runInNewContext } from 'node:vm';
5
+ import { transformSync } from 'esbuild';
6
6
  import { migrate } from './db-migrator.js';
7
7
  import { generateSchemaTypes } from './db-codegen.js';
8
8
  import { generateZodTypes } from './zod-codegen.js';
@@ -42,7 +42,11 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
42
42
  classificationMapFile: join(outDir, 'db', 'classification-map.gen.d.ts'),
43
43
  schemaJsonFile: join(outDir, 'db', 'pikku-db-schema.gen.json'),
44
44
  classificationsFile: join(rootDir, 'db', 'annotations.ts'),
45
- classificationsGenJsonFile: join(outDir, 'db', 'annotations.gen.json'),
45
+ // Compiled sidecar lives beside the authored annotations.ts in db/ — this is
46
+ // where both consumers read it: the codegen's loadAnnotations() and the
47
+ // pikku-console addon (db/annotations.gen.json). Writing it into outDir
48
+ // (.pikku) would leave both readers looking at a file that never appears.
49
+ classificationsGenJsonFile: join(rootDir, 'db', 'annotations.gen.json'),
46
50
  zodFile: join(outDir, 'db', 'zod.gen.ts'),
47
51
  camelCase: true,
48
52
  });
@@ -87,6 +91,9 @@ function resolveAgainst(root, p) {
87
91
  export async function migrateAndCodegen(resolved) {
88
92
  let migrateResult;
89
93
  let codegenResult;
94
+ // Compile any authored db/annotations.ts → sidecar BEFORE codegen so edits
95
+ // reflect in a single `db migrate` (codegen reads the sidecar).
96
+ compileClassifications(resolved.classificationsFile, resolved.classificationsGenJsonFile);
90
97
  if (resolved.dialect === 'sqlite') {
91
98
  mkdirSync(dirname(resolved.dbFile), { recursive: true });
92
99
  const runtime = await loadSqliteRuntime();
@@ -103,7 +110,7 @@ export async function migrateAndCodegen(resolved) {
103
110
  schemaJsonFile: resolved.schemaJsonFile,
104
111
  camelCase: resolved.camelCase,
105
112
  rootDir: resolved.rootDir,
106
- migrationsDir: resolved.migrationsDir,
113
+ dialect: 'sqlite',
107
114
  });
108
115
  }
109
116
  finally {
@@ -129,7 +136,7 @@ export async function migrateAndCodegen(resolved) {
129
136
  schemaJsonFile: resolved.schemaJsonFile,
130
137
  camelCase: resolved.camelCase,
131
138
  rootDir: resolved.rootDir,
132
- migrationsDir: resolved.migrationsDir,
139
+ dialect: 'postgres',
133
140
  });
134
141
  }
135
142
  finally {
@@ -143,9 +150,13 @@ export async function migrateAndCodegen(resolved) {
143
150
  const zodResult = generateZodTypes({
144
151
  schemaFile: resolved.schemaFile,
145
152
  outFile: resolved.zodFile,
153
+ formats: codegenResult.zodFormats,
146
154
  });
147
155
  // ── Classifications step ──────────────────────────────────────────────────
148
- const { scaffolded, jsonWritten } = syncClassifications(resolved.classificationsFile, resolved.classificationsGenJsonFile, codegenResult.tables);
156
+ // Scaffold the authored file if missing (needs the table list), then compile
157
+ // it to the sidecar so a freshly-scaffolded file is captured too.
158
+ const scaffolded = scaffoldClassificationsFile(resolved.classificationsFile, codegenResult.tables);
159
+ const jsonWritten = compileClassifications(resolved.classificationsFile, resolved.classificationsGenJsonFile);
149
160
  return {
150
161
  migrate: migrateResult,
151
162
  codegen: codegenResult,
@@ -179,86 +190,93 @@ export function reset(resolved, rootDir) {
179
190
  }
180
191
  // ── Classification sync ───────────────────────────────────────────────────────
181
192
  /**
182
- * Loads `db/classifications.ts` via tsx, serialises it to
183
- * `.pikku/db/classifications.gen.json` for runtime consumption (console, etc.).
184
- *
185
- * If `db/classifications.ts` doesn't exist yet, writes a scaffold with every
186
- * table defaulting to `private` so the developer has a starting point.
193
+ * If `db/annotations.ts` doesn't exist yet, write a scaffold listing every
194
+ * table/column so the developer has a typed starting point. Every field of
195
+ * `ColumnEntry` is optional, so the empty-per-table scaffold is valid and means
196
+ * "everything default `private`". Returns whether a scaffold was written.
187
197
  */
188
- function syncClassifications(classificationsFile, genJsonFile, tableNames) {
189
- let scaffolded = false;
190
- if (!existsSync(classificationsFile)) {
191
- const relMap = join(dirname(classificationsFile), '..', '.pikku', 'db', 'classification-map.gen.d.ts');
192
- const relMapPosix = relMap.replace(/\\/g, '/');
193
- const groups = new Map();
194
- for (const name of tableNames) {
195
- const dot = name.indexOf('.');
196
- const schema = dot >= 0 ? name.slice(0, dot) : '';
197
- const table = dot >= 0 ? name.slice(dot + 1) : name;
198
- if (!groups.has(schema))
199
- groups.set(schema, []);
200
- groups.get(schema).push(table);
201
- }
202
- const bodyLines = [
203
- `import type { DbClassificationMap } from '${relMapPosix}'`,
204
- ``,
205
- `export const classifications = {`,
206
- ];
207
- for (const [schema, tables] of groups) {
208
- if (schema)
209
- bodyLines.push(` ${JSON.stringify(schema)}: {`);
210
- for (const table of tables) {
211
- bodyLines.push(schema
212
- ? ` ${JSON.stringify(table)}: {`
213
- : ` ${JSON.stringify(table)}: {`);
214
- bodyLines.push(schema ? ` },` : ` },`);
215
- }
216
- if (schema)
217
- bodyLines.push(` },`);
198
+ function scaffoldClassificationsFile(classificationsFile, tableNames) {
199
+ if (existsSync(classificationsFile))
200
+ return false;
201
+ const relMap = join(dirname(classificationsFile), '..', '.pikku', 'db', 'classification-map.gen.d.ts');
202
+ const relMapPosix = relMap.replace(/\\/g, '/');
203
+ const groups = new Map();
204
+ for (const name of tableNames) {
205
+ const dot = name.indexOf('.');
206
+ const schema = dot >= 0 ? name.slice(0, dot) : '';
207
+ const table = dot >= 0 ? name.slice(dot + 1) : name;
208
+ if (!groups.has(schema))
209
+ groups.set(schema, []);
210
+ groups.get(schema).push(table);
211
+ }
212
+ const bodyLines = [
213
+ `import type { DbClassificationMap } from '${relMapPosix}'`,
214
+ ``,
215
+ `export const classifications = {`,
216
+ ];
217
+ for (const [schema, tables] of groups) {
218
+ if (schema)
219
+ bodyLines.push(` ${JSON.stringify(schema)}: {`);
220
+ for (const table of tables) {
221
+ bodyLines.push(schema
222
+ ? ` ${JSON.stringify(table)}: {`
223
+ : ` ${JSON.stringify(table)}: {`);
224
+ bodyLines.push(schema ? ` },` : ` },`);
218
225
  }
219
- bodyLines.push(`} satisfies DbClassificationMap`, ``);
220
- mkdirSync(dirname(classificationsFile), { recursive: true });
221
- writeFileSync(classificationsFile, bodyLines.join('\n'), 'utf8');
222
- scaffolded = true;
226
+ if (schema)
227
+ bodyLines.push(` },`);
223
228
  }
224
- // Resolve tsx from the CLI package's own node_modules so it works regardless
225
- // of whether the user's project has tsx installed.
226
- const _require = createRequire(fileURLToPath(import.meta.url));
227
- let tsxEsmPath = null;
229
+ bodyLines.push(`} satisfies DbClassificationMap`, ``);
230
+ mkdirSync(dirname(classificationsFile), { recursive: true });
231
+ writeFileSync(classificationsFile, bodyLines.join('\n'), 'utf8');
232
+ return true;
233
+ }
234
+ /**
235
+ * Compile `db/annotations.ts` into the `annotations.gen.json` sidecar that the
236
+ * codegen and the pikku-console addon read. No-op if the authored file doesn't
237
+ * exist (nothing to compile yet). Returns whether the sidecar changed on disk.
238
+ *
239
+ * Uses esbuild (a CLI dependency) to transpile the TS in-process and a `vm`
240
+ * sandbox to evaluate it — no subprocess and no tsx. The previous `node --import
241
+ * tsx/esm` subprocess silently fails on Node ≥ 23 (ERR_REQUIRE_CYCLE_MODULE),
242
+ * which is why this sidecar never materialised before.
243
+ *
244
+ * Run BEFORE codegen so authored edits reflect in a single `db migrate` (and
245
+ * again after, to capture a freshly-scaffolded file).
246
+ */
247
+ function compileClassifications(classificationsFile, genJsonFile) {
248
+ if (!existsSync(classificationsFile))
249
+ return false;
250
+ let value;
228
251
  try {
229
- tsxEsmPath = _require.resolve('tsx/esm');
252
+ const src = readFileSync(classificationsFile, 'utf8');
253
+ const { code } = transformSync(src, {
254
+ loader: 'ts',
255
+ format: 'cjs',
256
+ });
257
+ const mod = { exports: {} };
258
+ runInNewContext(code, {
259
+ module: mod,
260
+ exports: mod.exports,
261
+ require: createRequire(classificationsFile),
262
+ });
263
+ value = Object.values(mod.exports)[0];
230
264
  }
231
265
  catch {
232
- // tsx not bundled with this CLI install — skip JSON emit
266
+ return false; // syntax/transform error — skip JSON emit
233
267
  }
234
- const script = [
235
- `import * as mod from ${JSON.stringify(classificationsFile)}`,
236
- `const val = Object.values(mod)[0]`,
237
- `process.stdout.write(JSON.stringify(val))`,
238
- ].join('\n');
239
- let jsonWritten = false;
240
- if (tsxEsmPath) {
241
- try {
242
- const json = execSync(`node --import ${JSON.stringify(tsxEsmPath)} --input-type=module`, {
243
- input: script,
244
- encoding: 'utf8',
245
- stdio: ['pipe', 'pipe', 'pipe'],
246
- });
247
- const existing = existsSync(genJsonFile)
248
- ? readFileSync(genJsonFile, 'utf8')
249
- : null;
250
- const next = JSON.stringify(JSON.parse(json), null, 2) + '\n';
251
- if (existing !== next) {
252
- mkdirSync(dirname(genJsonFile), { recursive: true });
253
- writeFileSync(genJsonFile, next, 'utf8');
254
- jsonWritten = true;
255
- }
256
- }
257
- catch {
258
- // annotations file has syntax errors — skip JSON emit
259
- }
268
+ if (value === undefined)
269
+ return false;
270
+ const next = JSON.stringify(value, null, 2) + '\n';
271
+ const existing = existsSync(genJsonFile)
272
+ ? readFileSync(genJsonFile, 'utf8')
273
+ : null;
274
+ if (existing !== next) {
275
+ mkdirSync(dirname(genJsonFile), { recursive: true });
276
+ writeFileSync(genJsonFile, next, 'utf8');
277
+ return true;
260
278
  }
261
- return { scaffolded, jsonWritten };
279
+ return false;
262
280
  }
263
281
  export async function createKysely(resolved) {
264
282
  mkdirSync(dirname(resolved.dbFile), { recursive: true });
@@ -33,6 +33,7 @@ export class PostgresIntrospector {
33
33
  const result = await this.client.query(`SELECT
34
34
  c.column_name,
35
35
  c.data_type,
36
+ c.udt_name,
36
37
  c.is_nullable,
37
38
  c.column_default,
38
39
  c.is_generated,
@@ -54,6 +55,7 @@ export class PostgresIntrospector {
54
55
  return result.rows.map((r) => ({
55
56
  name: r.column_name,
56
57
  type: r.data_type,
58
+ udtName: r.udt_name,
57
59
  notNull: r.is_nullable === 'NO',
58
60
  pk: Boolean(r.is_pk),
59
61
  defaultValue: r.column_default,
@@ -1,6 +1,44 @@
1
+ /**
2
+ * Canonical map of `format` tokens (authored in `db/annotations.ts`) to the zod
3
+ * expression they emit. These are all *string* refinements — they do not change
4
+ * the column's TypeScript type (it stays `string`), only the runtime validator.
5
+ * This is the single source of truth: `annotation-parser` imports it to validate
6
+ * authored values and `db-codegen` imports the key list to emit the `format`
7
+ * union in `ColumnEntry`. All confirmed present in zod 4.
8
+ */
9
+ export declare const ZOD_FORMATS: {
10
+ readonly email: "z.email()";
11
+ readonly url: "z.url()";
12
+ readonly emoji: "z.emoji()";
13
+ readonly e164: "z.e164()";
14
+ readonly jwt: "z.jwt()";
15
+ readonly cuid: "z.cuid()";
16
+ readonly cuid2: "z.cuid2()";
17
+ readonly ulid: "z.ulid()";
18
+ readonly nanoid: "z.nanoid()";
19
+ readonly base64: "z.base64()";
20
+ readonly base64url: "z.base64url()";
21
+ readonly ipv4: "z.ipv4()";
22
+ readonly ipv6: "z.ipv6()";
23
+ readonly cidrv4: "z.cidrv4()";
24
+ readonly cidrv6: "z.cidrv6()";
25
+ readonly isoDate: "z.iso.date()";
26
+ readonly isoTime: "z.iso.time()";
27
+ readonly isoDatetime: "z.iso.datetime()";
28
+ readonly isoDuration: "z.iso.duration()";
29
+ };
30
+ export type ZodFormat = keyof typeof ZOD_FORMATS;
1
31
  export interface ZodCodegenOptions {
2
32
  schemaFile: string;
3
33
  outFile: string;
34
+ /**
35
+ * Per-interface, per-field `format` overrides. Keyed by the *interface* name
36
+ * (PascalCase, as it appears in `schema.d.ts`) and the *field* name (camelCase),
37
+ * matching how `parseTables` reads the schema. Computed by `db-codegen` (which
38
+ * owns the snake→Pascal/camel name mapping) so this emitter stays a dumb
39
+ * string→zod translator.
40
+ */
41
+ formats?: Record<string, Record<string, ZodFormat>>;
4
42
  }
5
43
  export interface ZodCodegenResult {
6
44
  outFile: string;
@@ -1,11 +1,40 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
+ /**
4
+ * Canonical map of `format` tokens (authored in `db/annotations.ts`) to the zod
5
+ * expression they emit. These are all *string* refinements — they do not change
6
+ * the column's TypeScript type (it stays `string`), only the runtime validator.
7
+ * This is the single source of truth: `annotation-parser` imports it to validate
8
+ * authored values and `db-codegen` imports the key list to emit the `format`
9
+ * union in `ColumnEntry`. All confirmed present in zod 4.
10
+ */
11
+ export const ZOD_FORMATS = {
12
+ email: 'z.email()',
13
+ url: 'z.url()',
14
+ emoji: 'z.emoji()',
15
+ e164: 'z.e164()',
16
+ jwt: 'z.jwt()',
17
+ cuid: 'z.cuid()',
18
+ cuid2: 'z.cuid2()',
19
+ ulid: 'z.ulid()',
20
+ nanoid: 'z.nanoid()',
21
+ base64: 'z.base64()',
22
+ base64url: 'z.base64url()',
23
+ ipv4: 'z.ipv4()',
24
+ ipv6: 'z.ipv6()',
25
+ cidrv4: 'z.cidrv4()',
26
+ cidrv6: 'z.cidrv6()',
27
+ isoDate: 'z.iso.date()',
28
+ isoTime: 'z.iso.time()',
29
+ isoDatetime: 'z.iso.datetime()',
30
+ isoDuration: 'z.iso.duration()',
31
+ };
3
32
  const INTERFACE_RE = /export\s+interface\s+(\w+)\s*\{([^}]*)\}/g;
4
33
  const FIELD_RE = /^\s*(\w+)\s*:\s*(.+?)\s*$/gm;
5
34
  export function generateZodTypes(options) {
6
35
  const src = readFileSync(options.schemaFile, 'utf8');
7
36
  const tables = parseTables(src);
8
- const body = emitZodModule(tables);
37
+ const body = emitZodModule(tables, options.formats ?? {});
9
38
  let existing = null;
10
39
  try {
11
40
  existing = readFileSync(options.outFile, 'utf8');
@@ -39,7 +68,7 @@ function parseTables(src) {
39
68
  }
40
69
  return tables;
41
70
  }
42
- function emitZodModule(tables) {
71
+ function emitZodModule(tables, formats) {
43
72
  const lines = [];
44
73
  lines.push('// Generated by @pikku/cli — do not edit by hand.');
45
74
  lines.push('// Run `pikku db migrate` to refresh.');
@@ -49,8 +78,9 @@ function emitZodModule(tables) {
49
78
  for (const table of tables) {
50
79
  const rowFields = [];
51
80
  const insertFields = [];
81
+ const tableFormats = formats[table.name] ?? {};
52
82
  for (const field of table.fields) {
53
- const { schema, generated } = zodForType(field.type);
83
+ const { schema, generated } = zodForType(field.type, tableFormats[field.name]);
54
84
  rowFields.push(` ${field.name}: ${schema},`);
55
85
  insertFields.push(` ${field.name}: ${schema}${generated ? '.optional()' : ''},`);
56
86
  }
@@ -67,43 +97,200 @@ function emitZodModule(tables) {
67
97
  }
68
98
  return `${lines.join('\n')}\n`;
69
99
  }
70
- function zodForType(tsType) {
71
- const generatedMatch = tsType.match(/^Generated<(.+)>$/);
72
- const generated = generatedMatch !== null;
73
- let inner = generated ? generatedMatch[1].trim() : tsType;
100
+ /**
101
+ * Resolve a Kysely field type to a `{ schema, generated }` pair, where `schema`
102
+ * is the zod expression for the *row* (select) shape and `generated` is true
103
+ * when the column is optional on insert.
104
+ *
105
+ * `db-codegen` emits two field shapes depending on a column's classification:
106
+ * - public columns → `Generated<T>`, bare `T`, `T | null`, or a public
107
+ * bool/date column as `Generated<ColumnType<base, rw, rw>>`
108
+ * - private/pii/secret columns (the default is `private`) →
109
+ * `ColumnType<Brand<base> | null, insert | undefined, update>`
110
+ * where Brand is `Private` | `Pii` | `Secret`.
111
+ *
112
+ * For `ColumnType<Select, Insert, Update>` the row schema comes from `Select`
113
+ * and the column is insert-optional when `Insert` admits `undefined` (Kysely's
114
+ * encoding for default/auto/generated columns).
115
+ */
116
+ function zodForType(tsType, format) {
117
+ let inner = tsType.trim();
118
+ let generated = false;
119
+ // Peel a single `Generated<…>` wrapper. For public bool/date columns this
120
+ // wraps a `ColumnType<…>`, so the unwrapped inner is handled below.
121
+ const generatedMatch = inner.match(/^Generated<(.+)>$/);
122
+ if (generatedMatch) {
123
+ generated = true;
124
+ inner = generatedMatch[1].trim();
125
+ }
126
+ // `ColumnType<Select, Insert, Update>` — classified columns and wrapped
127
+ // public bool/date columns. Row schema from Select; optional from Insert.
128
+ if (inner.startsWith('ColumnType<') && inner.endsWith('>')) {
129
+ const args = splitGenericArgs(inner.slice('ColumnType<'.length, -1));
130
+ const selectT = args[0]?.trim() ?? 'unknown';
131
+ const insertT = args[1]?.trim() ?? '';
132
+ if (unionIncludesUndefined(insertT)) {
133
+ generated = true;
134
+ }
135
+ return { schema: scalarSchema(selectT, format), generated };
136
+ }
137
+ return { schema: scalarSchema(inner, format), generated };
138
+ }
139
+ /**
140
+ * Resolve a scalar/select type expression to a zod expression. Handles a
141
+ * trailing `| null`, classification brands (`Private`/`Pii`/`Secret`), arrays,
142
+ * and the known scalar bases. Unknown bases fall back to `z.unknown()` so
143
+ * generation stays total.
144
+ */
145
+ function scalarSchema(tsType, format) {
146
+ let inner = tsType.trim();
147
+ // Defensive: a Select arg may itself be `Generated<…>` in older schemas.
148
+ const generatedMatch = inner.match(/^Generated<(.+)>$/);
149
+ if (generatedMatch) {
150
+ inner = generatedMatch[1].trim();
151
+ }
74
152
  const nullable = inner.endsWith(' | null');
75
153
  if (nullable) {
76
154
  inner = inner.slice(0, -' | null'.length).trim();
77
155
  }
156
+ // Unwrap a classification brand: `Private<T>` / `Pii<T>` / `Secret<T>` → T.
157
+ const brandMatch = inner.match(/^(?:Private|Pii|Secret)<(.+)>$/);
158
+ if (brandMatch) {
159
+ inner = brandMatch[1].trim();
160
+ }
78
161
  let schema;
79
- switch (inner) {
80
- case 'string':
81
- schema = 'z.string()';
82
- break;
83
- case 'number':
84
- schema = 'z.number()';
85
- break;
86
- case 'boolean':
87
- schema = 'z.boolean()';
88
- break;
89
- case 'Date':
90
- schema = 'z.date()';
91
- break;
92
- case 'unknown':
93
- schema = 'z.unknown()';
94
- break;
95
- default:
96
- if (inner.endsWith('[]')) {
97
- const item = zodForType(inner.slice(0, -2).trim()).schema;
98
- schema = `z.array(${item})`;
99
- }
100
- else {
101
- schema = 'z.unknown()';
162
+ // A `format` override (e.g. `email`, `url`) replaces the string base with a
163
+ // refined string validator. `db-codegen` only emits a format hint for columns
164
+ // whose select type is plain `string`, so this never collides with Date/enum.
165
+ if (format) {
166
+ schema = ZOD_FORMATS[format];
167
+ }
168
+ else {
169
+ // An enum column is emitted as a union of string literals, e.g.
170
+ // `'admin' | 'user'`. Detect it (after brand-unwrap + null-peel) and map to
171
+ // `z.enum([...])` (or `z.literal(...)` for a single value).
172
+ const literals = stringLiteralUnion(inner);
173
+ if (literals) {
174
+ schema =
175
+ literals.length === 1
176
+ ? `z.literal('${literals[0]}')`
177
+ : `z.enum([${literals.map((l) => `'${l}'`).join(', ')}])`;
178
+ }
179
+ else {
180
+ switch (inner) {
181
+ case 'string':
182
+ schema = 'z.string()';
183
+ break;
184
+ case 'number':
185
+ schema = 'z.number()';
186
+ break;
187
+ case 'boolean':
188
+ schema = 'z.boolean()';
189
+ break;
190
+ case 'Date':
191
+ schema = 'z.date()';
192
+ break;
193
+ case 'Uuid':
194
+ schema = 'z.uuid()';
195
+ break;
196
+ case 'unknown':
197
+ schema = 'z.unknown()';
198
+ break;
199
+ default:
200
+ if (inner.endsWith('[]')) {
201
+ schema = `z.array(${scalarSchema(inner.slice(0, -2).trim())})`;
202
+ }
203
+ else {
204
+ schema = 'z.unknown()';
205
+ }
206
+ break;
102
207
  }
103
- break;
208
+ }
104
209
  }
105
210
  if (nullable) {
106
211
  schema += '.nullable()';
107
212
  }
108
- return { schema, generated };
213
+ return schema;
214
+ }
215
+ /**
216
+ * If `s` is a union of single-quoted string literals (`'a' | 'b' | 'c'`),
217
+ * return the unescaped-as-written label list; otherwise null. Quote-aware so a
218
+ * label containing `|` does not split the union. Re-emitting wraps each captured
219
+ * group back in quotes, so escaping is preserved verbatim.
220
+ */
221
+ function stringLiteralUnion(s) {
222
+ const parts = splitUnion(s);
223
+ const labels = [];
224
+ for (const part of parts) {
225
+ const match = part.trim().match(/^'((?:[^'\\]|\\.)*)'$/);
226
+ if (!match)
227
+ return null;
228
+ labels.push(match[1]);
229
+ }
230
+ return labels.length > 0 ? labels : null;
231
+ }
232
+ /** Split a union on top-level `|`, skipping `|` inside `'…'` strings or `<>`/`()`. */
233
+ function splitUnion(s) {
234
+ const parts = [];
235
+ let depth = 0;
236
+ let inStr = false;
237
+ let start = 0;
238
+ for (let i = 0; i < s.length; i++) {
239
+ const char = s[i];
240
+ if (inStr) {
241
+ if (char === '\\')
242
+ i++;
243
+ else if (char === "'")
244
+ inStr = false;
245
+ continue;
246
+ }
247
+ if (char === "'")
248
+ inStr = true;
249
+ else if (char === '<' || char === '(')
250
+ depth++;
251
+ else if (char === '>' || char === ')')
252
+ depth--;
253
+ else if (char === '|' && depth === 0) {
254
+ parts.push(s.slice(start, i));
255
+ start = i + 1;
256
+ }
257
+ }
258
+ parts.push(s.slice(start));
259
+ return parts;
260
+ }
261
+ /**
262
+ * Split the generic argument list of `Foo<a, b, c>` on top-level commas.
263
+ * Quote-aware so a comma inside an enum label literal (`'a,b'`) does not split.
264
+ */
265
+ function splitGenericArgs(args) {
266
+ const parts = [];
267
+ let depth = 0;
268
+ let inStr = false;
269
+ let start = 0;
270
+ for (let i = 0; i < args.length; i++) {
271
+ const char = args[i];
272
+ if (inStr) {
273
+ if (char === '\\')
274
+ i++;
275
+ else if (char === "'")
276
+ inStr = false;
277
+ continue;
278
+ }
279
+ if (char === "'")
280
+ inStr = true;
281
+ else if (char === '<')
282
+ depth++;
283
+ else if (char === '>')
284
+ depth--;
285
+ else if (char === ',' && depth === 0) {
286
+ parts.push(args.slice(start, i));
287
+ start = i + 1;
288
+ }
289
+ }
290
+ parts.push(args.slice(start));
291
+ return parts;
292
+ }
293
+ /** True when a union type expression contains `undefined` as a top-level member. */
294
+ function unionIncludesUndefined(union) {
295
+ return union.split('|').some((member) => member.trim() === 'undefined');
109
296
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.33
2
+ * This file was generated by @pikku/cli@0.12.35
3
3
  */
4
4
  /**
5
5
  * Auto-generated remote internal RPC queue worker and HTTP endpoint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/cli",
3
- "version": "0.12.33",
3
+ "version": "0.12.35",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "imports": {
@@ -26,10 +26,10 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
29
- "@pikku/core": "^0.12.30",
29
+ "@pikku/core": "^0.12.31",
30
30
  "@pikku/deploy-cloudflare": "^0.12.3",
31
31
  "@pikku/fetch": "^0.12.3",
32
- "@pikku/inspector": "^0.12.18",
32
+ "@pikku/inspector": "^0.12.19",
33
33
  "@pikku/kysely": "^0.12.14",
34
34
  "@pikku/kysely-node-sqlite": "^0.12.1",
35
35
  "@pikku/node-http-server": "^0.12.2",