@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.
- package/console-app/assets/index-BOM3RFeu.js +233 -0
- 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-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +24 -24
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +2 -2
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +3 -3
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/functions/db/annotation-parser.d.ts +27 -16
- package/dist/src/functions/db/annotation-parser.js +50 -110
- package/dist/src/functions/db/coercion-plugin.d.ts +1 -1
- package/dist/src/functions/db/coercion-plugin.js +4 -0
- package/dist/src/functions/db/db-codegen.d.ts +13 -1
- package/dist/src/functions/db/db-codegen.js +149 -34
- package/dist/src/functions/db/db-introspector.d.ts +6 -0
- package/dist/src/functions/db/local-db.js +96 -78
- package/dist/src/functions/db/postgres/postgres-introspector.js +2 -0
- package/dist/src/functions/db/zod-codegen.d.ts +38 -0
- package/dist/src/functions/db/zod-codegen.js +219 -32
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/package.json +3 -3
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
writeFileSync(classificationsFile, bodyLines.join('\n'), 'utf8');
|
|
222
|
-
scaffolded = true;
|
|
226
|
+
if (schema)
|
|
227
|
+
bodyLines.push(` },`);
|
|
223
228
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
+
return false; // syntax/transform error — skip JSON emit
|
|
233
267
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
208
|
+
}
|
|
104
209
|
}
|
|
105
210
|
if (nullable) {
|
|
106
211
|
schema += '.nullable()';
|
|
107
212
|
}
|
|
108
|
-
return
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/cli",
|
|
3
|
-
"version": "0.12.
|
|
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.
|
|
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.
|
|
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",
|