@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,29 +1,44 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { ZOD_FORMATS } from './zod-codegen.js';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Warn-only naming heuristic. We no longer *infer* a column's kind from its
|
|
6
|
+
* name (it produced wrong types — e.g. SQLite stores `*_at` as ISO TEXT, not a
|
|
7
|
+
* `Date`). Instead the codegen warns when a column name looks like it wants a
|
|
8
|
+
* `kind` but none is declared in `db/annotations.ts`, so the developer can opt
|
|
9
|
+
* in explicitly. Returns the *suggested* kind, or null.
|
|
7
10
|
*/
|
|
8
|
-
export function
|
|
11
|
+
export function nameSuggestsKind(colName) {
|
|
9
12
|
if (/_at$|_on$/.test(colName))
|
|
10
|
-
return
|
|
13
|
+
return 'date';
|
|
11
14
|
if (/^is_|^has_|^can_/.test(colName))
|
|
12
|
-
return
|
|
15
|
+
return 'bool';
|
|
13
16
|
return null;
|
|
14
17
|
}
|
|
15
|
-
|
|
18
|
+
function parseStrategy(s) {
|
|
19
|
+
if (!s)
|
|
20
|
+
return null;
|
|
21
|
+
const valid = ['fake:email', 'fake:name', 'hash', 'keep'];
|
|
22
|
+
return valid.includes(s)
|
|
23
|
+
? s
|
|
24
|
+
: null;
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
27
|
+
* Load annotations from the `db/annotations.gen.json` sidecar, which is
|
|
28
|
+
* compiled from the developer-authored `db/annotations.ts` (`DbClassificationMap`)
|
|
29
|
+
* by `syncClassifications`. This is the single source of column classification
|
|
30
|
+
* and type-override information — there is no SQL-comment fallback.
|
|
19
31
|
*
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
32
|
+
* The authored `ColumnEntry` shape is:
|
|
33
|
+
* { security?, classification?: <anonymize strategy>, kind?, tsType?, description? }
|
|
34
|
+
* where `security` is the privacy level and `classification` is the anonymize
|
|
35
|
+
* strategy. Returns `{}` if the sidecar doesn't exist yet (first migrate run,
|
|
36
|
+
* before it has been generated).
|
|
22
37
|
*/
|
|
23
|
-
function
|
|
38
|
+
export function loadAnnotations(rootDir) {
|
|
24
39
|
const jsonPath = join(rootDir, 'db', 'annotations.gen.json');
|
|
25
40
|
if (!existsSync(jsonPath))
|
|
26
|
-
return
|
|
41
|
+
return {};
|
|
27
42
|
try {
|
|
28
43
|
const raw = JSON.parse(readFileSync(jsonPath, 'utf8'));
|
|
29
44
|
const result = {};
|
|
@@ -33,111 +48,36 @@ function loadAnnotationsSidecar(rootDir) {
|
|
|
33
48
|
if (!ann)
|
|
34
49
|
continue;
|
|
35
50
|
const entry = {};
|
|
36
|
-
if (ann.kind === 'bool' ||
|
|
51
|
+
if (ann.kind === 'bool' ||
|
|
52
|
+
ann.kind === 'date' ||
|
|
53
|
+
ann.kind === 'json' ||
|
|
54
|
+
ann.kind === 'uuid')
|
|
37
55
|
entry.kind = ann.kind;
|
|
38
56
|
if (ann.tsType)
|
|
39
57
|
entry.tsType = ann.tsType;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
if (ann.format && ann.format in ZOD_FORMATS)
|
|
59
|
+
entry.format = ann.format;
|
|
60
|
+
// `security` is the privacy level. `encrypted` brands as `secret`.
|
|
61
|
+
switch (ann.security) {
|
|
62
|
+
case 'public':
|
|
63
|
+
case 'private':
|
|
64
|
+
case 'pii':
|
|
65
|
+
case 'secret':
|
|
66
|
+
entry.classification = ann.security;
|
|
67
|
+
break;
|
|
68
|
+
case 'encrypted':
|
|
69
|
+
entry.classification = 'secret';
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
const strategy = parseStrategy(ann.classification);
|
|
73
|
+
if (strategy !== null)
|
|
74
|
+
entry.anonymize = strategy;
|
|
43
75
|
result[table][col] = entry;
|
|
44
76
|
}
|
|
45
77
|
}
|
|
46
78
|
return result;
|
|
47
79
|
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// ── SQL comment parsing (fallback) ───────────────────────────────────────────
|
|
53
|
-
function parseStrategy(s) {
|
|
54
|
-
if (!s)
|
|
55
|
-
return null;
|
|
56
|
-
const valid = ['fake:email', 'fake:name', 'hash', 'keep'];
|
|
57
|
-
return valid.includes(s)
|
|
58
|
-
? s
|
|
59
|
-
: null;
|
|
60
|
-
}
|
|
61
|
-
function parseComment(comment) {
|
|
62
|
-
const ann = {};
|
|
63
|
-
if (/@bool\b/i.test(comment)) {
|
|
64
|
-
ann.kind = 'bool';
|
|
65
|
-
}
|
|
66
|
-
else if (/@date\b/i.test(comment)) {
|
|
67
|
-
ann.kind = 'date';
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
const jsonM = comment.match(/@json\b(?:\s+([^\s@]+))?/i);
|
|
71
|
-
if (jsonM) {
|
|
72
|
-
ann.kind = 'json';
|
|
73
|
-
if (jsonM[1])
|
|
74
|
-
ann.tsType = jsonM[1].trim();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const classM = comment.match(/@(public|private|pii|secret)(?::([^\s@]+))?/i);
|
|
78
|
-
if (classM) {
|
|
79
|
-
ann.classification = classM[1].toLowerCase();
|
|
80
|
-
const strategy = parseStrategy(classM[2]);
|
|
81
|
-
if (strategy !== null)
|
|
82
|
-
ann.anonymize = strategy;
|
|
83
|
-
}
|
|
84
|
-
return ann;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @pii[:strategy] | @secret[:strategy]`
|
|
88
|
-
* inline annotations from migration SQL files in `migrationsDir`.
|
|
89
|
-
*/
|
|
90
|
-
export function parseAnnotations(migrationsDir) {
|
|
91
|
-
let files;
|
|
92
|
-
try {
|
|
93
|
-
files = readdirSync(migrationsDir)
|
|
94
|
-
.filter((f) => f.endsWith('.sql'))
|
|
95
|
-
.sort();
|
|
96
|
-
}
|
|
97
80
|
catch {
|
|
98
81
|
return {};
|
|
99
82
|
}
|
|
100
|
-
const result = {};
|
|
101
|
-
function merge(tableName, colName, partial) {
|
|
102
|
-
if (!partial.kind && partial.classification === undefined)
|
|
103
|
-
return;
|
|
104
|
-
if (!result[tableName])
|
|
105
|
-
result[tableName] = {};
|
|
106
|
-
result[tableName][colName] = { ...result[tableName][colName], ...partial };
|
|
107
|
-
}
|
|
108
|
-
for (const file of files) {
|
|
109
|
-
const content = readFileSync(join(migrationsDir, file), 'utf8');
|
|
110
|
-
const createTablePattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?(\w+)"?\s*\(([^;]+)\)/gis;
|
|
111
|
-
let tableMatch;
|
|
112
|
-
while ((tableMatch = createTablePattern.exec(content)) !== null) {
|
|
113
|
-
const tableName = tableMatch[1].toLowerCase();
|
|
114
|
-
const body = tableMatch[2];
|
|
115
|
-
for (const line of body.split('\n')) {
|
|
116
|
-
const trimmed = line.trim();
|
|
117
|
-
if (/^(PRIMARY|UNIQUE|CHECK|FOREIGN|CONSTRAINT)/i.test(trimmed))
|
|
118
|
-
continue;
|
|
119
|
-
const lineMatch = trimmed.match(/^(\w+)\s+\w[^-]*--\s*(.+?)\s*,?\s*$/);
|
|
120
|
-
if (!lineMatch)
|
|
121
|
-
continue;
|
|
122
|
-
merge(tableName, lineMatch[1].toLowerCase(), parseComment(lineMatch[2]));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const alterPattern = /ALTER\s+TABLE\s+"?(\w+)"?\s+ADD\s+(?:COLUMN\s+)?"?(\w+)"?\s+\w[^;\n-]*(?:;\s*)?--\s*(.+?)(?:\r?\n|$)/gim;
|
|
126
|
-
let alterMatch;
|
|
127
|
-
while ((alterMatch = alterPattern.exec(content)) !== null) {
|
|
128
|
-
merge(alterMatch[1].toLowerCase(), alterMatch[2].toLowerCase(), parseComment(alterMatch[3]));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
// ── Public entry point ────────────────────────────────────────────────────────
|
|
134
|
-
/**
|
|
135
|
-
* Load annotations for a project. Tries `db/annotations.ts` sidecar first;
|
|
136
|
-
* falls back to SQL comment parsing from `migrationsDir` if not found.
|
|
137
|
-
*/
|
|
138
|
-
export function loadAnnotations(rootDir, migrationsDir) {
|
|
139
|
-
const sidecar = loadAnnotationsSidecar(rootDir);
|
|
140
|
-
if (sidecar)
|
|
141
|
-
return sidecar;
|
|
142
|
-
return migrationsDir ? parseAnnotations(migrationsDir) : {};
|
|
143
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KyselyPlugin } from 'kysely';
|
|
2
|
-
export type ColumnKind = 'date' | 'bool' | 'json';
|
|
2
|
+
export type ColumnKind = 'date' | 'bool' | 'json' | 'uuid';
|
|
3
3
|
export type CoercionMap = Record<string, Record<string, ColumnKind>>;
|
|
4
4
|
export interface CreateCoercionPluginOptions {
|
|
5
5
|
map: CoercionMap;
|
|
@@ -23,6 +23,10 @@ function fromDb(value, kind) {
|
|
|
23
23
|
catch {
|
|
24
24
|
return value;
|
|
25
25
|
}
|
|
26
|
+
case 'uuid':
|
|
27
|
+
// UUIDs are strings in both Postgres and SQLite — no runtime coercion.
|
|
28
|
+
// (Codegen also omits `uuid` from the coercion map; this is defensive.)
|
|
29
|
+
return value;
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
function snakeToCamel(name) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DbIntrospector } from './db-introspector.js';
|
|
2
|
+
import { type ZodFormat } from './zod-codegen.js';
|
|
3
|
+
type Dialect = 'sqlite' | 'postgres';
|
|
2
4
|
export interface CodegenOptions {
|
|
3
5
|
outFile: string;
|
|
4
6
|
coercionFile: string;
|
|
@@ -7,7 +9,8 @@ export interface CodegenOptions {
|
|
|
7
9
|
schemaJsonFile?: string;
|
|
8
10
|
camelCase?: boolean;
|
|
9
11
|
rootDir?: string;
|
|
10
|
-
|
|
12
|
+
/** DB dialect — drives real-type-aware date typing. Defaults to 'sqlite'. */
|
|
13
|
+
dialect?: Dialect;
|
|
11
14
|
}
|
|
12
15
|
export interface CodegenResult {
|
|
13
16
|
outFile: string;
|
|
@@ -19,6 +22,14 @@ export interface CodegenResult {
|
|
|
19
22
|
manifestWritten: boolean;
|
|
20
23
|
classificationMapWritten: boolean;
|
|
21
24
|
tables: string[];
|
|
25
|
+
/** Non-fatal codegen warnings (e.g. name looks like a date but unannotated). */
|
|
26
|
+
warnings: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Per-interface, per-field zod `format` overrides for the zod codegen. Keyed
|
|
29
|
+
* by interface name (PascalCase) and field name (camelCase), matching the
|
|
30
|
+
* shapes the zod emitter parses out of `schema.d.ts`.
|
|
31
|
+
*/
|
|
32
|
+
zodFormats: Record<string, Record<string, ZodFormat>>;
|
|
22
33
|
}
|
|
23
34
|
/**
|
|
24
35
|
* Introspect `introspector` and emit:
|
|
@@ -27,3 +38,4 @@ export interface CodegenResult {
|
|
|
27
38
|
* - `classification.gen.ts` Data-classification manifest (when manifestFile set)
|
|
28
39
|
*/
|
|
29
40
|
export declare function generateSchemaTypes(introspector: DbIntrospector, options: CodegenOptions): Promise<CodegenResult>;
|
|
41
|
+
export {};
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { loadAnnotations,
|
|
3
|
+
import { loadAnnotations, nameSuggestsKind, } from './annotation-parser.js';
|
|
4
|
+
import { ZOD_FORMATS } from './zod-codegen.js';
|
|
5
|
+
/**
|
|
6
|
+
* The column kind implied by the *real* DB type, dialect-aware. Postgres has
|
|
7
|
+
* native temporal/boolean types so we can trust them; SQLite stores dates as
|
|
8
|
+
* TEXT and booleans as INTEGER, so its declared types are indeterminate and we
|
|
9
|
+
* derive nothing (return null). Used both to auto-type Postgres dates and to
|
|
10
|
+
* detect name↔type contradictions for warnings.
|
|
11
|
+
*/
|
|
12
|
+
function realKind(dialect, sqlType) {
|
|
13
|
+
if (dialect !== 'postgres')
|
|
14
|
+
return null;
|
|
15
|
+
const u = sqlType.toUpperCase();
|
|
16
|
+
if (u.includes('TIMESTAMP') || u === 'DATE')
|
|
17
|
+
return 'date';
|
|
18
|
+
if (u === 'BOOLEAN' || u === 'BOOL')
|
|
19
|
+
return 'bool';
|
|
20
|
+
if (u === 'UUID')
|
|
21
|
+
return 'uuid';
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
4
24
|
// ─── Name helpers ─────────────────────────────────────────────────────────────
|
|
5
25
|
function snakeToPascal(name) {
|
|
6
26
|
return name
|
|
@@ -11,6 +31,10 @@ function snakeToPascal(name) {
|
|
|
11
31
|
function snakeToCamel(name) {
|
|
12
32
|
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
13
33
|
}
|
|
34
|
+
/** Escape a value for embedding inside a single-quoted TS string literal. */
|
|
35
|
+
function escapeTsString(value) {
|
|
36
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
37
|
+
}
|
|
14
38
|
// ─── Type mapping ─────────────────────────────────────────────────────────────
|
|
15
39
|
function mapType(sqlType) {
|
|
16
40
|
const upper = sqlType.toUpperCase();
|
|
@@ -40,21 +64,30 @@ function mapType(sqlType) {
|
|
|
40
64
|
}
|
|
41
65
|
// ─── Type expression ─────────────────────────────────────────────────────────
|
|
42
66
|
function selectBase(annotation, col) {
|
|
67
|
+
// An explicit `tsType` is a general type override and wins over everything.
|
|
68
|
+
if (annotation?.tsType)
|
|
69
|
+
return annotation.tsType;
|
|
43
70
|
if (annotation?.kind === 'bool')
|
|
44
71
|
return 'boolean';
|
|
45
72
|
if (annotation?.kind === 'date')
|
|
46
73
|
return 'Date';
|
|
74
|
+
if (annotation?.kind === 'uuid')
|
|
75
|
+
return 'Uuid';
|
|
47
76
|
if (annotation?.kind === 'json')
|
|
48
|
-
return
|
|
77
|
+
return 'unknown';
|
|
49
78
|
return mapType(col.type);
|
|
50
79
|
}
|
|
51
80
|
function insertBase(annotation, col) {
|
|
81
|
+
if (annotation?.tsType)
|
|
82
|
+
return annotation.tsType;
|
|
52
83
|
if (annotation?.kind === 'bool')
|
|
53
84
|
return 'boolean | number';
|
|
54
85
|
if (annotation?.kind === 'date')
|
|
55
86
|
return 'Date | string';
|
|
87
|
+
if (annotation?.kind === 'uuid')
|
|
88
|
+
return 'Uuid';
|
|
56
89
|
if (annotation?.kind === 'json')
|
|
57
|
-
return
|
|
90
|
+
return 'unknown';
|
|
58
91
|
return mapType(col.type);
|
|
59
92
|
}
|
|
60
93
|
function columnTypeExpression(col, annotation, classification) {
|
|
@@ -73,14 +106,17 @@ function columnTypeExpression(col, annotation, classification) {
|
|
|
73
106
|
const rw = nullable ? 'Date | string | null' : 'Date | string';
|
|
74
107
|
return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
|
|
75
108
|
}
|
|
109
|
+
if (annotation?.kind === 'uuid') {
|
|
110
|
+
return wrap(nullable ? 'Uuid | null' : 'Uuid');
|
|
111
|
+
}
|
|
112
|
+
if (annotation?.tsType) {
|
|
113
|
+
const base = nullable
|
|
114
|
+
? `${annotation.tsType} | null`
|
|
115
|
+
: annotation.tsType;
|
|
116
|
+
return wrap(base);
|
|
117
|
+
}
|
|
76
118
|
if (annotation?.kind === 'json') {
|
|
77
|
-
const base =
|
|
78
|
-
? nullable
|
|
79
|
-
? `${annotation.tsType} | null`
|
|
80
|
-
: annotation.tsType
|
|
81
|
-
: nullable
|
|
82
|
-
? 'unknown | null'
|
|
83
|
-
: 'unknown';
|
|
119
|
+
const base = nullable ? 'unknown | null' : 'unknown';
|
|
84
120
|
return wrap(base);
|
|
85
121
|
}
|
|
86
122
|
const base = mapType(col.type);
|
|
@@ -109,18 +145,63 @@ function bareTableName(name) {
|
|
|
109
145
|
const dot = name.indexOf('.');
|
|
110
146
|
return dot >= 0 ? name.slice(dot + 1) : name;
|
|
111
147
|
}
|
|
112
|
-
function emitInterface(table, camelCase, explicitAnnotations) {
|
|
148
|
+
function emitInterface(table, camelCase, explicitAnnotations, dialect, enumByName, formatHints, warnings) {
|
|
113
149
|
const ifaceName = snakeToPascal(table.name);
|
|
114
|
-
const
|
|
150
|
+
const bare = bareTableName(table.name);
|
|
151
|
+
const tableCols = explicitAnnotations[bare] ?? {};
|
|
115
152
|
const fields = table.columns
|
|
116
153
|
.map((col) => {
|
|
117
154
|
const fieldName = camelCase ? snakeToCamel(col.name) : col.name;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
155
|
+
const ann = tableCols[col.name] ?? null;
|
|
156
|
+
// Effective typing kind: explicit annotation wins; otherwise, on Postgres
|
|
157
|
+
// the *real* column type tells us (a timestamp genuinely is a Date). On
|
|
158
|
+
// SQLite there is no native date storage (dates are TEXT), so nothing is
|
|
159
|
+
// derived — `string` unless explicitly `kind: 'date'`.
|
|
160
|
+
// On Postgres, real `timestamp`/`uuid` types carry through automatically
|
|
161
|
+
// (no annotation needed); SQLite has neither native type so derives nothing.
|
|
162
|
+
const real = realKind(dialect, col.type);
|
|
163
|
+
const derived = !ann?.tsType && (real === 'date' || real === 'uuid') ? real : undefined;
|
|
164
|
+
const typingKind = ann?.kind ?? derived;
|
|
165
|
+
// Warn (don't force) only on a genuine contradiction the real type can
|
|
166
|
+
// prove: a column NAMED like a date/bool whose actual type disagrees
|
|
167
|
+
// (e.g. `created_at` that is really a boolean in Postgres). On SQLite the
|
|
168
|
+
// type is indeterminate, so there is nothing to contradict — no warning.
|
|
169
|
+
if (!ann?.kind && !ann?.tsType) {
|
|
170
|
+
const suggested = nameSuggestsKind(col.name);
|
|
171
|
+
if (suggested && real && suggested !== real) {
|
|
172
|
+
warnings.push(`Column "${bare}.${col.name}" is named like a ${suggested} but its DB type ` +
|
|
173
|
+
`is ${col.type} (${real}). If intentional, set its kind in db/annotations.ts.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// A Postgres enum column reports `type` as 'USER-DEFINED'; its real values
|
|
177
|
+
// come from `udtName`. Type it as a union of string literals — only when
|
|
178
|
+
// no explicit `tsType`/`kind` overrides it. This reuses the `tsType`
|
|
179
|
+
// plumbing, so it flows through both the public and classified branches.
|
|
180
|
+
const enumValues = col.udtName ? enumByName.get(col.udtName) : undefined;
|
|
181
|
+
const enumUnion = enumValues && enumValues.length > 0
|
|
182
|
+
? enumValues.map((v) => `'${escapeTsString(v)}'`).join(' | ')
|
|
183
|
+
: null;
|
|
184
|
+
const effectiveTsType = ann?.tsType ?? (typingKind ? undefined : (enumUnion ?? undefined));
|
|
185
|
+
const typeAnn = typingKind || effectiveTsType
|
|
186
|
+
? { kind: typingKind, tsType: effectiveTsType }
|
|
187
|
+
: null;
|
|
188
|
+
// A `format` validator refines the zod schema only and keeps the TS type
|
|
189
|
+
// as `string`. It therefore applies only when the resolved select base is
|
|
190
|
+
// plain `string`; on anything else (Date/Uuid/boolean/enum/unknown via
|
|
191
|
+
// kind/tsType) it would contradict the type, so warn and skip.
|
|
192
|
+
if (ann?.format) {
|
|
193
|
+
const base = selectBase(typeAnn, col);
|
|
194
|
+
if (base === 'string') {
|
|
195
|
+
;
|
|
196
|
+
(formatHints[ifaceName] ??= {})[fieldName] = ann.format;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
warnings.push(`Column "${bare}.${col.name}": format '${ann.format}' ignored — its ` +
|
|
200
|
+
`resolved type is ${base}, not string. Remove the conflicting kind/tsType.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const classification = ann?.classification ?? 'private';
|
|
204
|
+
const type = columnTypeExpression(col, typeAnn, classification);
|
|
124
205
|
const safeName = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName)
|
|
125
206
|
? fieldName
|
|
126
207
|
: JSON.stringify(fieldName);
|
|
@@ -168,7 +249,21 @@ function emitManifest(tables, explicitAnnotations) {
|
|
|
168
249
|
* flag added or removed columns.
|
|
169
250
|
*/
|
|
170
251
|
function emitClassificationMap(tables) {
|
|
171
|
-
const colEntry =
|
|
252
|
+
const colEntry = [
|
|
253
|
+
` /** Privacy level. Defaults to 'private' when omitted. */`,
|
|
254
|
+
` security?: 'public' | 'private' | 'pii' | 'secret' | 'encrypted'`,
|
|
255
|
+
` /** Anonymize strategy used by \`pikku db anonymize\`. */`,
|
|
256
|
+
` classification?: 'fake:email' | 'fake:name' | 'hash' | 'keep'`,
|
|
257
|
+
` /** Column kind override for codegen coercion + typing. */`,
|
|
258
|
+
` kind?: 'date' | 'bool' | 'json' | 'uuid'`,
|
|
259
|
+
` /** TypeScript type override, e.g. \`string[]\` or \`MyJson\`. Wins over \`kind\`. */`,
|
|
260
|
+
` tsType?: string`,
|
|
261
|
+
` /** Zod string-format validator (keeps the TS type as \`string\`). */`,
|
|
262
|
+
` format?: ${Object.keys(ZOD_FORMATS)
|
|
263
|
+
.map((f) => `'${f}'`)
|
|
264
|
+
.join(' | ')}`,
|
|
265
|
+
` description?: string`,
|
|
266
|
+
].join('\n');
|
|
172
267
|
// Group tables by schema (for postgres schema.table names)
|
|
173
268
|
const schemaMap = new Map();
|
|
174
269
|
for (const table of tables) {
|
|
@@ -225,20 +320,32 @@ function emitClassificationMap(tables) {
|
|
|
225
320
|
*/
|
|
226
321
|
export async function generateSchemaTypes(introspector, options) {
|
|
227
322
|
const camelCase = options.camelCase ?? true;
|
|
323
|
+
const dialect = options.dialect ?? 'sqlite';
|
|
228
324
|
const tableNames = await introspector.listTables();
|
|
229
325
|
const tables = await Promise.all(tableNames.map(async (name) => ({
|
|
230
326
|
name,
|
|
231
327
|
columns: await introspector.getColumns(name),
|
|
232
328
|
})));
|
|
233
329
|
const explicitAnnotations = options.rootDir
|
|
234
|
-
? loadAnnotations(options.rootDir
|
|
235
|
-
:
|
|
236
|
-
|
|
237
|
-
|
|
330
|
+
? loadAnnotations(options.rootDir)
|
|
331
|
+
: {};
|
|
332
|
+
// Enum types — used to auto-type enum columns as string-literal unions. Keyed
|
|
333
|
+
// by both bare and schema-qualified name; `udtName` is bare so the bare key
|
|
334
|
+
// resolves it (a one-line changeset note flags the cross-schema-name caveat).
|
|
335
|
+
const enums = await introspector.listEnums();
|
|
336
|
+
const enumByName = new Map();
|
|
337
|
+
for (const e of enums) {
|
|
338
|
+
enumByName.set(e.name, e.values);
|
|
339
|
+
enumByName.set(`${e.schema}.${e.name}`, e.values);
|
|
340
|
+
}
|
|
238
341
|
// ── schema.d.ts ─────────────────────────────────────────────────────────────
|
|
342
|
+
const warnings = [];
|
|
343
|
+
const zodFormats = {};
|
|
239
344
|
const interfaces = tables
|
|
240
|
-
.map((t) => emitInterface(t, camelCase, explicitAnnotations))
|
|
345
|
+
.map((t) => emitInterface(t, camelCase, explicitAnnotations, dialect, enumByName, zodFormats, warnings))
|
|
241
346
|
.join('\n\n');
|
|
347
|
+
for (const w of warnings)
|
|
348
|
+
console.warn(`[pikku db] ${w}`);
|
|
242
349
|
const dbEntries = tables
|
|
243
350
|
.map((t) => {
|
|
244
351
|
const tableKey = camelCase ? snakeToCamel(t.name) : t.name;
|
|
@@ -258,9 +365,16 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
258
365
|
` ? ColumnType<S, I | undefined, U>`,
|
|
259
366
|
` : ColumnType<T, T | undefined, T>`,
|
|
260
367
|
``,
|
|
261
|
-
`
|
|
262
|
-
|
|
263
|
-
|
|
368
|
+
// `__classification__` is optional so plain values stay assignable to branded
|
|
369
|
+
// columns (Kysely where/insert/update operands) while the brand remains
|
|
370
|
+
// structurally detectable for the inspector's PKU910 output check. Keep this
|
|
371
|
+
// in lockstep with `@pikku/core`'s data-classification.ts definitions.
|
|
372
|
+
`export type Private<T> = T & { readonly __classification__?: 'private' }`,
|
|
373
|
+
`export type Pii<T> = T & { readonly __classification__?: 'pii' }`,
|
|
374
|
+
`export type Secret<T> = T & { readonly __classification__?: 'secret' }`,
|
|
375
|
+
// Transparent alias (structurally a string, so plain strings stay
|
|
376
|
+
// interchangeable) — its name lets the zod codegen emit `z.uuid()`.
|
|
377
|
+
`export type Uuid = string`,
|
|
264
378
|
``,
|
|
265
379
|
interfaces,
|
|
266
380
|
``,
|
|
@@ -274,9 +388,11 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
274
388
|
for (const table of tables) {
|
|
275
389
|
const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
|
|
276
390
|
for (const col of table.columns) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
391
|
+
// Coercion is driven only by an explicit `kind` in db/annotations.ts —
|
|
392
|
+
// no name inference. An unannotated `*_at` column is not coerced. `uuid`
|
|
393
|
+
// is a string in both dialects, so it needs no runtime coercion.
|
|
394
|
+
const kind = tableCols[col.name]?.kind;
|
|
395
|
+
if (kind && kind !== 'uuid') {
|
|
280
396
|
if (!coercionMap[table.name])
|
|
281
397
|
coercionMap[table.name] = {};
|
|
282
398
|
coercionMap[table.name][col.name] = kind;
|
|
@@ -311,10 +427,7 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
311
427
|
// ── pikku-db-schema.gen.json ─────────────────────────────────────────────────
|
|
312
428
|
let schemaJsonBody = null;
|
|
313
429
|
if (options.schemaJsonFile) {
|
|
314
|
-
const
|
|
315
|
-
Promise.all(tableNames.map((name) => introspector.getForeignKeys(name))),
|
|
316
|
-
introspector.listEnums(),
|
|
317
|
-
]);
|
|
430
|
+
const fkResults = await Promise.all(tableNames.map((name) => introspector.getForeignKeys(name)));
|
|
318
431
|
const fkMap = new Map(tableNames.map((name, i) => [name, fkResults[i]]));
|
|
319
432
|
const jsonTables = tables.map((t) => ({
|
|
320
433
|
name: t.name,
|
|
@@ -419,5 +532,7 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
419
532
|
manifestWritten: manifestChanged,
|
|
420
533
|
classificationMapWritten: classificationMapChanged,
|
|
421
534
|
tables: tables.map((t) => t.name),
|
|
535
|
+
warnings,
|
|
536
|
+
zodFormats,
|
|
422
537
|
};
|
|
423
538
|
}
|
|
@@ -7,6 +7,12 @@ export interface ColumnInfo {
|
|
|
7
7
|
defaultValue: string | null;
|
|
8
8
|
/** True for virtual or stored generated columns — these are read-only and never inserted. */
|
|
9
9
|
generated?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Underlying DB type name (Postgres `udt_name`). For an enum column `type` is
|
|
12
|
+
* the generic `'USER-DEFINED'`, while `udtName` holds the actual enum type
|
|
13
|
+
* name used to resolve its values. Undefined on SQLite (no native enums).
|
|
14
|
+
*/
|
|
15
|
+
udtName?: string;
|
|
10
16
|
}
|
|
11
17
|
export interface ForeignKeyInfo {
|
|
12
18
|
column: string;
|