@pikku/cli 0.12.26 → 0.12.28
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/cli.schema.json +1 -1
- package/console-app/assets/index-Ca6xJwNm.js +229 -0
- package/console-app/assets/{index-C52h1B_L.css → index-DwUzVI5k.css} +1 -1
- package/console-app/index.html +2 -2
- 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 +192 -170
- package/dist/.pikku/function/pikku-functions.gen.js +3 -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-meta.gen.json +4 -0
- 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 +11 -10
- 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 +13 -13
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +8 -2
- 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/fabric/functions/validate-core.js +6 -6
- package/dist/src/fabric/functions/validate.function.js +23 -7
- package/dist/src/functions/commands/tests-coverage.js +4 -2
- package/dist/src/functions/db/annotation-parser.d.ts +7 -7
- package/dist/src/functions/db/annotation-parser.js +61 -11
- package/dist/src/functions/db/db-codegen.d.ts +4 -0
- package/dist/src/functions/db/db-codegen.js +117 -15
- package/dist/src/functions/db/local-db.d.ts +6 -0
- package/dist/src/functions/db/local-db.js +134 -34
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +8 -2
- package/dist/src/functions/db/postgres/postgres-introspector.js +26 -14
- package/dist/src/functions/validate/workspace-validate.js +4 -1
- package/dist/src/functions/wirings/auth/pikku-command-auth.d.ts +1 -0
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +22 -0
- package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +1 -0
- package/dist/src/functions/wirings/auth/serialize-auth-gen.js +115 -0
- package/dist/src/functions/workflows/all.workflow.js +1 -0
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/utils/pikku-cli-config.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/skills/pikku-auth-js/SKILL.md +137 -117
- package/skills/pikku-middleware/SKILL.md +283 -0
- package/skills/pikku-permissions/SKILL.md +165 -0
- package/skills/pikku-security/SKILL.md +38 -177
- package/skills/pikku-services/SKILL.md +44 -7
- package/skills/pikku-tag-middleware/SKILL.md +13 -0
- package/console-app/assets/index-Ba9K10XZ.js +0 -232
|
@@ -111,8 +111,8 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
|
|
|
111
111
|
logger.error(`Verbose metadata not found at ${verboseMetaPath}. Run 'pikku all' first.`);
|
|
112
112
|
process.exit(1);
|
|
113
113
|
}
|
|
114
|
-
const coverageFinal = join(functionsDir, 'coverage', 'coverage-final.json');
|
|
115
|
-
const outDir = join(ftestDir, 'coverage');
|
|
114
|
+
const coverageFinal = join(functionsDir, '.coverage', 'coverage-final.json');
|
|
115
|
+
const outDir = join(ftestDir, '.coverage');
|
|
116
116
|
const outFile = join(outDir, 'function-coverage.json');
|
|
117
117
|
if (!noRun) {
|
|
118
118
|
const findBin = (name, searchFrom) => {
|
|
@@ -153,6 +153,8 @@ export const pikkuTestsCoverage = pikkuSessionlessFunc({
|
|
|
153
153
|
'src',
|
|
154
154
|
'--include',
|
|
155
155
|
'src/**',
|
|
156
|
+
'--report-dir',
|
|
157
|
+
'.coverage',
|
|
156
158
|
'--reporter',
|
|
157
159
|
'json',
|
|
158
160
|
'--reporter',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ColumnKind } from './coercion-plugin.js';
|
|
2
|
-
type Classification = 'public' | 'private' | 'secret';
|
|
2
|
+
type Classification = 'public' | 'private' | 'pii' | 'secret';
|
|
3
3
|
type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null;
|
|
4
4
|
export interface ColAnnotation {
|
|
5
5
|
kind?: ColumnKind;
|
|
@@ -19,13 +19,13 @@ export declare function annotationFromName(colName: string): {
|
|
|
19
19
|
kind: ColumnKind;
|
|
20
20
|
} | null;
|
|
21
21
|
/**
|
|
22
|
-
* Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
|
|
22
|
+
* Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @pii[:strategy] | @secret[:strategy]`
|
|
23
23
|
* inline annotations from migration SQL files in `migrationsDir`.
|
|
24
|
-
*
|
|
25
|
-
* Multiple annotations on the same comment line are supported, e.g.:
|
|
26
|
-
* `deleted_at TIMESTAMP -- @date @private:keep`
|
|
27
|
-
*
|
|
28
|
-
* Covers both CREATE TABLE body lines and ALTER TABLE ... ADD [COLUMN] statements.
|
|
29
24
|
*/
|
|
30
25
|
export declare function parseAnnotations(migrationsDir: string): AnnotationMap;
|
|
26
|
+
/**
|
|
27
|
+
* Load annotations for a project. Tries `db/annotations.ts` sidecar first;
|
|
28
|
+
* falls back to SQL comment parsing from `migrationsDir` if not found.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadAnnotations(rootDir: string, migrationsDir?: string): AnnotationMap;
|
|
31
31
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
/**
|
|
4
4
|
* Determine column kind from naming conventions:
|
|
@@ -12,11 +12,51 @@ export function annotationFromName(colName) {
|
|
|
12
12
|
return { kind: 'bool' };
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
|
+
// ── Load from db/annotations.gen.json sidecar ────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Try to load annotations from a `db/annotations.gen.json` sidecar generated
|
|
18
|
+
* by `yarn db:types`. Returns null if the file doesn't exist.
|
|
19
|
+
*
|
|
20
|
+
* The JSON file uses snake_case keys (raw DB names) so it can be read
|
|
21
|
+
* directly without any conversion. It is emitted by bin/db-classify.ts.
|
|
22
|
+
*/
|
|
23
|
+
function loadAnnotationsSidecar(rootDir) {
|
|
24
|
+
const jsonPath = join(rootDir, 'db', 'annotations.gen.json');
|
|
25
|
+
if (!existsSync(jsonPath))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(readFileSync(jsonPath, 'utf8'));
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const [table, cols] of Object.entries(raw)) {
|
|
31
|
+
result[table] = {};
|
|
32
|
+
for (const [col, ann] of Object.entries(cols)) {
|
|
33
|
+
if (!ann)
|
|
34
|
+
continue;
|
|
35
|
+
const entry = {};
|
|
36
|
+
if (ann.kind === 'bool' || ann.kind === 'date' || ann.kind === 'json')
|
|
37
|
+
entry.kind = ann.kind;
|
|
38
|
+
if (ann.tsType)
|
|
39
|
+
entry.tsType = ann.tsType;
|
|
40
|
+
const vis = ann.visibility;
|
|
41
|
+
if (vis === 'public' || vis === 'private' || vis === 'secret')
|
|
42
|
+
entry.classification = vis;
|
|
43
|
+
result[table][col] = entry;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ── SQL comment parsing (fallback) ───────────────────────────────────────────
|
|
15
53
|
function parseStrategy(s) {
|
|
16
54
|
if (!s)
|
|
17
55
|
return null;
|
|
18
56
|
const valid = ['fake:email', 'fake:name', 'hash', 'keep'];
|
|
19
|
-
return valid.includes(s)
|
|
57
|
+
return valid.includes(s)
|
|
58
|
+
? s
|
|
59
|
+
: null;
|
|
20
60
|
}
|
|
21
61
|
function parseComment(comment) {
|
|
22
62
|
const ann = {};
|
|
@@ -34,26 +74,25 @@ function parseComment(comment) {
|
|
|
34
74
|
ann.tsType = jsonM[1].trim();
|
|
35
75
|
}
|
|
36
76
|
}
|
|
37
|
-
const classM = comment.match(/@(public|private|secret)(?::([^\s@]+))?/i);
|
|
77
|
+
const classM = comment.match(/@(public|private|pii|secret)(?::([^\s@]+))?/i);
|
|
38
78
|
if (classM) {
|
|
39
79
|
ann.classification = classM[1].toLowerCase();
|
|
40
|
-
|
|
80
|
+
const strategy = parseStrategy(classM[2]);
|
|
81
|
+
if (strategy !== null)
|
|
82
|
+
ann.anonymize = strategy;
|
|
41
83
|
}
|
|
42
84
|
return ann;
|
|
43
85
|
}
|
|
44
86
|
/**
|
|
45
|
-
* Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @secret[:strategy]`
|
|
87
|
+
* Parse `-- @bool | @date | @json [TsType] | @public | @private[:strategy] | @pii[:strategy] | @secret[:strategy]`
|
|
46
88
|
* inline annotations from migration SQL files in `migrationsDir`.
|
|
47
|
-
*
|
|
48
|
-
* Multiple annotations on the same comment line are supported, e.g.:
|
|
49
|
-
* `deleted_at TIMESTAMP -- @date @private:keep`
|
|
50
|
-
*
|
|
51
|
-
* Covers both CREATE TABLE body lines and ALTER TABLE ... ADD [COLUMN] statements.
|
|
52
89
|
*/
|
|
53
90
|
export function parseAnnotations(migrationsDir) {
|
|
54
91
|
let files;
|
|
55
92
|
try {
|
|
56
|
-
files = readdirSync(migrationsDir)
|
|
93
|
+
files = readdirSync(migrationsDir)
|
|
94
|
+
.filter((f) => f.endsWith('.sql'))
|
|
95
|
+
.sort();
|
|
57
96
|
}
|
|
58
97
|
catch {
|
|
59
98
|
return {};
|
|
@@ -91,3 +130,14 @@ export function parseAnnotations(migrationsDir) {
|
|
|
91
130
|
}
|
|
92
131
|
return result;
|
|
93
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
|
+
}
|
|
@@ -3,16 +3,20 @@ export interface CodegenOptions {
|
|
|
3
3
|
outFile: string;
|
|
4
4
|
coercionFile: string;
|
|
5
5
|
manifestFile?: string;
|
|
6
|
+
classificationMapFile?: string;
|
|
6
7
|
camelCase?: boolean;
|
|
8
|
+
rootDir?: string;
|
|
7
9
|
migrationsDir?: string;
|
|
8
10
|
}
|
|
9
11
|
export interface CodegenResult {
|
|
10
12
|
outFile: string;
|
|
11
13
|
coercionFile: string;
|
|
12
14
|
manifestFile?: string;
|
|
15
|
+
classificationMapFile?: string;
|
|
13
16
|
written: boolean;
|
|
14
17
|
coercionWritten: boolean;
|
|
15
18
|
manifestWritten: boolean;
|
|
19
|
+
classificationMapWritten: boolean;
|
|
16
20
|
tables: string[];
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { parseAnnotations, annotationFromName, } from './annotation-parser.js';
|
|
3
|
+
import { loadAnnotations, parseAnnotations, annotationFromName, } from './annotation-parser.js';
|
|
4
4
|
// ─── Name helpers ─────────────────────────────────────────────────────────────
|
|
5
5
|
function snakeToPascal(name) {
|
|
6
6
|
return name
|
|
@@ -23,7 +23,9 @@ function mapType(sqlType) {
|
|
|
23
23
|
return 'string';
|
|
24
24
|
if (upper.includes('BLOB') || upper === 'BYTEA')
|
|
25
25
|
return 'Buffer';
|
|
26
|
-
if (upper.includes('REAL') ||
|
|
26
|
+
if (upper.includes('REAL') ||
|
|
27
|
+
upper.includes('FLOA') ||
|
|
28
|
+
upper.includes('DOUB'))
|
|
27
29
|
return 'number';
|
|
28
30
|
if (upper.includes('NUMERIC') || upper.includes('DECIMAL'))
|
|
29
31
|
return 'number';
|
|
@@ -88,7 +90,11 @@ function columnTypeExpression(col, annotation, classification) {
|
|
|
88
90
|
return `Generated<${base}${nullable ? ' | null' : ''}>`;
|
|
89
91
|
return nullable ? `${base} | null` : base;
|
|
90
92
|
}
|
|
91
|
-
const B = classification === 'secret'
|
|
93
|
+
const B = classification === 'secret'
|
|
94
|
+
? 'Secret'
|
|
95
|
+
: classification === 'pii'
|
|
96
|
+
? 'Pii'
|
|
97
|
+
: 'Private';
|
|
92
98
|
const sBase = selectBase(annotation, col);
|
|
93
99
|
const iBase = insertBase(annotation, col);
|
|
94
100
|
const selectT = nullable ? `${B}<${sBase}> | null` : `${B}<${sBase}>`;
|
|
@@ -98,9 +104,14 @@ function columnTypeExpression(col, annotation, classification) {
|
|
|
98
104
|
const updateT = nullable ? `${iBase} | null` : iBase;
|
|
99
105
|
return `ColumnType<${selectT}, ${insertT}, ${updateT}>`;
|
|
100
106
|
}
|
|
107
|
+
/** Strip optional schema prefix (e.g. "app.user" → "user"). */
|
|
108
|
+
function bareTableName(name) {
|
|
109
|
+
const dot = name.indexOf('.');
|
|
110
|
+
return dot >= 0 ? name.slice(dot + 1) : name;
|
|
111
|
+
}
|
|
101
112
|
function emitInterface(table, camelCase, explicitAnnotations) {
|
|
102
113
|
const ifaceName = snakeToPascal(table.name);
|
|
103
|
-
const tableCols = explicitAnnotations[table.name] ?? {};
|
|
114
|
+
const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
|
|
104
115
|
const fields = table.columns
|
|
105
116
|
.map((col) => {
|
|
106
117
|
const fieldName = camelCase ? snakeToCamel(col.name) : col.name;
|
|
@@ -122,7 +133,7 @@ function emitInterface(table, camelCase, explicitAnnotations) {
|
|
|
122
133
|
function emitManifest(tables, explicitAnnotations) {
|
|
123
134
|
const tableEntries = tables
|
|
124
135
|
.map((table) => {
|
|
125
|
-
const tableCols = explicitAnnotations[table.name] ?? {};
|
|
136
|
+
const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
|
|
126
137
|
const colEntries = table.columns
|
|
127
138
|
.map((col) => {
|
|
128
139
|
const ann = tableCols[col.name];
|
|
@@ -149,6 +160,63 @@ function emitManifest(tables, explicitAnnotations) {
|
|
|
149
160
|
``,
|
|
150
161
|
].join('\n');
|
|
151
162
|
}
|
|
163
|
+
// ─── Classification map type emitter ─────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Emits a `DbClassificationMap` type declaration that the developer's
|
|
166
|
+
* hand-authored `db/classifications.ts` must satisfy. Every table and column
|
|
167
|
+
* present in the current schema appears as a required key — TypeScript will
|
|
168
|
+
* flag added or removed columns.
|
|
169
|
+
*/
|
|
170
|
+
function emitClassificationMap(tables) {
|
|
171
|
+
const colEntry = ` security: 'public' | 'private' | 'pii' | 'secret' | 'encrypted'\n classification?: 'fake:email' | 'fake:name' | 'hash' | 'keep'\n description?: string`;
|
|
172
|
+
// Group tables by schema (for postgres schema.table names)
|
|
173
|
+
const schemaMap = new Map();
|
|
174
|
+
for (const table of tables) {
|
|
175
|
+
const dot = table.name.indexOf('.');
|
|
176
|
+
const schema = dot >= 0 ? table.name.slice(0, dot) : '';
|
|
177
|
+
const bare = dot >= 0 ? table.name.slice(dot + 1) : table.name;
|
|
178
|
+
if (!schemaMap.has(schema))
|
|
179
|
+
schemaMap.set(schema, new Map());
|
|
180
|
+
schemaMap.get(schema).set(bare, table.columns.map((c) => c.name));
|
|
181
|
+
}
|
|
182
|
+
const lines = [
|
|
183
|
+
`// Generated by @pikku/cli — do not edit by hand.`,
|
|
184
|
+
`// Run \`pikku db migrate\` to refresh.`,
|
|
185
|
+
`// Use this type in db/classifications.ts:`,
|
|
186
|
+
`// import type { DbClassificationMap } from './.pikku/db/classification-map.gen.d.ts'`,
|
|
187
|
+
`// export const classifications = { ... } satisfies DbClassificationMap`,
|
|
188
|
+
``,
|
|
189
|
+
`export type ColumnEntry = {`,
|
|
190
|
+
`${colEntry}`,
|
|
191
|
+
`}`,
|
|
192
|
+
``,
|
|
193
|
+
`export type DbClassificationMap = {`,
|
|
194
|
+
];
|
|
195
|
+
for (const [schema, tables] of schemaMap) {
|
|
196
|
+
if (schema) {
|
|
197
|
+
lines.push(` ${JSON.stringify(schema)}: {`);
|
|
198
|
+
for (const [table, cols] of tables) {
|
|
199
|
+
lines.push(` ${JSON.stringify(table)}: {`);
|
|
200
|
+
for (const col of cols) {
|
|
201
|
+
lines.push(` ${JSON.stringify(col)}: ColumnEntry`);
|
|
202
|
+
}
|
|
203
|
+
lines.push(` }`);
|
|
204
|
+
}
|
|
205
|
+
lines.push(` }`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
for (const [table, cols] of tables) {
|
|
209
|
+
lines.push(` ${JSON.stringify(table)}: {`);
|
|
210
|
+
for (const col of cols) {
|
|
211
|
+
lines.push(` ${JSON.stringify(col)}: ColumnEntry`);
|
|
212
|
+
}
|
|
213
|
+
lines.push(` }`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push(`}`, ``);
|
|
218
|
+
return lines.join('\n');
|
|
219
|
+
}
|
|
152
220
|
/**
|
|
153
221
|
* Introspect `introspector` and emit:
|
|
154
222
|
* - `schema.d.ts` Kysely DB type with classification brands
|
|
@@ -162,9 +230,11 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
162
230
|
name,
|
|
163
231
|
columns: await introspector.getColumns(name),
|
|
164
232
|
})));
|
|
165
|
-
const explicitAnnotations = options.
|
|
166
|
-
?
|
|
167
|
-
:
|
|
233
|
+
const explicitAnnotations = options.rootDir
|
|
234
|
+
? loadAnnotations(options.rootDir, options.migrationsDir)
|
|
235
|
+
: options.migrationsDir
|
|
236
|
+
? parseAnnotations(options.migrationsDir)
|
|
237
|
+
: {};
|
|
168
238
|
// ── schema.d.ts ─────────────────────────────────────────────────────────────
|
|
169
239
|
const interfaces = tables
|
|
170
240
|
.map((t) => emitInterface(t, camelCase, explicitAnnotations))
|
|
@@ -188,8 +258,9 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
188
258
|
` ? ColumnType<S, I | undefined, U>`,
|
|
189
259
|
` : ColumnType<T, T | undefined, T>`,
|
|
190
260
|
``,
|
|
191
|
-
`export type Private<T> = T & { readonly
|
|
192
|
-
`export type
|
|
261
|
+
`export type Private<T> = T & { readonly __classification__: 'private' }`,
|
|
262
|
+
`export type Pii<T> = T & { readonly __classification__: 'pii' }`,
|
|
263
|
+
`export type Secret<T> = T & { readonly __classification__: 'secret' }`,
|
|
193
264
|
``,
|
|
194
265
|
interfaces,
|
|
195
266
|
``,
|
|
@@ -201,7 +272,7 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
201
272
|
// ── coercion.gen.ts ──────────────────────────────────────────────────────────
|
|
202
273
|
const coercionMap = {};
|
|
203
274
|
for (const table of tables) {
|
|
204
|
-
const tableCols = explicitAnnotations[table.name] ?? {};
|
|
275
|
+
const tableCols = explicitAnnotations[bareTableName(table.name)] ?? {};
|
|
205
276
|
for (const col of table.columns) {
|
|
206
277
|
const sqlAnn = tableCols[col.name];
|
|
207
278
|
const kind = sqlAnn?.kind ?? annotationFromName(col.name)?.kind;
|
|
@@ -230,28 +301,51 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
230
301
|
``,
|
|
231
302
|
].join('\n');
|
|
232
303
|
// ── classification.gen.ts ───────────────────────────────────────────────────
|
|
233
|
-
const manifestBody = options.manifestFile
|
|
304
|
+
const manifestBody = options.manifestFile
|
|
305
|
+
? emitManifest(tables, explicitAnnotations)
|
|
306
|
+
: null;
|
|
307
|
+
// ── classification-map.gen.d.ts ──────────────────────────────────────────────
|
|
308
|
+
const classificationMapBody = options.classificationMapFile
|
|
309
|
+
? emitClassificationMap(tables)
|
|
310
|
+
: null;
|
|
234
311
|
// ── write files ───────────────────────────────────────────────────────────────
|
|
235
312
|
let existingSchema = null;
|
|
236
313
|
let existingCoercion = null;
|
|
237
314
|
let existingManifest = null;
|
|
315
|
+
let existingClassificationMap = null;
|
|
238
316
|
try {
|
|
239
317
|
existingSchema = readFileSync(options.outFile, 'utf8');
|
|
240
318
|
}
|
|
241
|
-
catch {
|
|
319
|
+
catch {
|
|
320
|
+
/* ok */
|
|
321
|
+
}
|
|
242
322
|
try {
|
|
243
323
|
existingCoercion = readFileSync(options.coercionFile, 'utf8');
|
|
244
324
|
}
|
|
245
|
-
catch {
|
|
325
|
+
catch {
|
|
326
|
+
/* ok */
|
|
327
|
+
}
|
|
246
328
|
if (options.manifestFile) {
|
|
247
329
|
try {
|
|
248
330
|
existingManifest = readFileSync(options.manifestFile, 'utf8');
|
|
249
331
|
}
|
|
250
|
-
catch {
|
|
332
|
+
catch {
|
|
333
|
+
/* ok */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (options.classificationMapFile) {
|
|
337
|
+
try {
|
|
338
|
+
existingClassificationMap = readFileSync(options.classificationMapFile, 'utf8');
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
/* ok */
|
|
342
|
+
}
|
|
251
343
|
}
|
|
252
344
|
const schemaChanged = existingSchema !== schemaBody;
|
|
253
345
|
const coercionChanged = existingCoercion !== coercionBody;
|
|
254
346
|
const manifestChanged = manifestBody !== null && existingManifest !== manifestBody;
|
|
347
|
+
const classificationMapChanged = classificationMapBody !== null &&
|
|
348
|
+
existingClassificationMap !== classificationMapBody;
|
|
255
349
|
if (schemaChanged) {
|
|
256
350
|
mkdirSync(dirname(options.outFile), { recursive: true });
|
|
257
351
|
writeFileSync(options.outFile, schemaBody, 'utf8');
|
|
@@ -264,13 +358,21 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
264
358
|
mkdirSync(dirname(options.manifestFile), { recursive: true });
|
|
265
359
|
writeFileSync(options.manifestFile, manifestBody, 'utf8');
|
|
266
360
|
}
|
|
361
|
+
if (classificationMapChanged &&
|
|
362
|
+
options.classificationMapFile &&
|
|
363
|
+
classificationMapBody) {
|
|
364
|
+
mkdirSync(dirname(options.classificationMapFile), { recursive: true });
|
|
365
|
+
writeFileSync(options.classificationMapFile, classificationMapBody, 'utf8');
|
|
366
|
+
}
|
|
267
367
|
return {
|
|
268
368
|
outFile: options.outFile,
|
|
269
369
|
coercionFile: options.coercionFile,
|
|
270
370
|
manifestFile: options.manifestFile,
|
|
371
|
+
classificationMapFile: options.classificationMapFile,
|
|
271
372
|
written: schemaChanged,
|
|
272
373
|
coercionWritten: coercionChanged,
|
|
273
374
|
manifestWritten: manifestChanged,
|
|
375
|
+
classificationMapWritten: classificationMapChanged,
|
|
274
376
|
tables: tables.map((t) => t.name),
|
|
275
377
|
};
|
|
276
378
|
}
|
|
@@ -5,10 +5,14 @@ import { type ZodCodegenResult } from './zod-codegen.js';
|
|
|
5
5
|
import { type SeedResult } from './sqlite/seed.js';
|
|
6
6
|
import type { UserConfigShape } from '../commands/db-shared.js';
|
|
7
7
|
interface ResolvedDbBase {
|
|
8
|
+
rootDir: string;
|
|
8
9
|
migrationsDir: string;
|
|
9
10
|
schemaFile: string;
|
|
10
11
|
coercionFile: string;
|
|
11
12
|
manifestFile: string;
|
|
13
|
+
classificationMapFile: string;
|
|
14
|
+
classificationsFile: string;
|
|
15
|
+
classificationsGenJsonFile: string;
|
|
12
16
|
zodFile: string;
|
|
13
17
|
camelCase: boolean;
|
|
14
18
|
}
|
|
@@ -34,6 +38,8 @@ export interface MigrateAndCodegenOutcome {
|
|
|
34
38
|
migrate: MigrateResult;
|
|
35
39
|
codegen: CodegenResult;
|
|
36
40
|
zod: ZodCodegenResult;
|
|
41
|
+
classificationsScaffolded: boolean;
|
|
42
|
+
classificationsJsonWritten: boolean;
|
|
37
43
|
}
|
|
38
44
|
export declare function migrateAndCodegen(resolved: ResolvedDb): Promise<MigrateAndCodegenOutcome>;
|
|
39
45
|
export declare function seed(resolved: ResolvedSqliteDb): Promise<SeedResult>;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
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
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
import { migrate } from './db-migrator.js';
|
|
4
7
|
import { generateSchemaTypes } from './db-codegen.js';
|
|
5
8
|
import { generateZodTypes } from './zod-codegen.js';
|
|
@@ -18,10 +21,14 @@ import { PostgresIntrospector } from './postgres/postgres-introspector.js';
|
|
|
18
21
|
*/
|
|
19
22
|
export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
|
|
20
23
|
const base = (sub) => ({
|
|
24
|
+
rootDir,
|
|
21
25
|
migrationsDir: resolveAgainst(rootDir, sub),
|
|
22
26
|
schemaFile: join(outDir, 'db', 'schema.d.ts'),
|
|
23
27
|
coercionFile: join(outDir, 'db', 'coercion.gen.ts'),
|
|
24
28
|
manifestFile: join(outDir, 'db', 'classification.gen.ts'),
|
|
29
|
+
classificationMapFile: join(outDir, 'db', 'classification-map.gen.d.ts'),
|
|
30
|
+
classificationsFile: join(rootDir, 'db', 'annotations.ts'),
|
|
31
|
+
classificationsGenJsonFile: join(outDir, 'db', 'annotations.gen.json'),
|
|
25
32
|
zodFile: join(outDir, 'db', 'zod.gen.ts'),
|
|
26
33
|
camelCase: true,
|
|
27
34
|
});
|
|
@@ -43,7 +50,7 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
|
|
|
43
50
|
dialect: 'sqlite',
|
|
44
51
|
dbFile: resolveAgainst(rootDir, userConfig.sqliteDb),
|
|
45
52
|
runtimeDir: resolvedRuntimeDir,
|
|
46
|
-
seedFile: resolveAgainst(rootDir, 'db/seed.sql'),
|
|
53
|
+
seedFile: resolveAgainst(rootDir, 'db/sqlite-seed.sql'),
|
|
47
54
|
...base('db/sqlite'),
|
|
48
55
|
};
|
|
49
56
|
}
|
|
@@ -60,61 +67,71 @@ function resolveAgainst(root, p) {
|
|
|
60
67
|
return isAbsolute(p) ? p : resolve(root, p);
|
|
61
68
|
}
|
|
62
69
|
export async function migrateAndCodegen(resolved) {
|
|
70
|
+
let migrateResult;
|
|
71
|
+
let codegenResult;
|
|
63
72
|
if (resolved.dialect === 'sqlite') {
|
|
64
73
|
mkdirSync(dirname(resolved.dbFile), { recursive: true });
|
|
65
74
|
const runtime = await loadSqliteRuntime();
|
|
66
75
|
const db = runtime.open(resolved.dbFile);
|
|
67
76
|
try {
|
|
68
77
|
const executor = new SqliteMigrationExecutor(db);
|
|
69
|
-
|
|
78
|
+
migrateResult = await migrate(executor, resolved.migrationsDir);
|
|
70
79
|
const introspector = new SqliteIntrospector(db);
|
|
71
|
-
|
|
80
|
+
codegenResult = await generateSchemaTypes(introspector, {
|
|
72
81
|
outFile: resolved.schemaFile,
|
|
73
82
|
coercionFile: resolved.coercionFile,
|
|
74
83
|
manifestFile: resolved.manifestFile,
|
|
84
|
+
classificationMapFile: resolved.classificationMapFile,
|
|
75
85
|
camelCase: resolved.camelCase,
|
|
86
|
+
rootDir: resolved.rootDir,
|
|
76
87
|
migrationsDir: resolved.migrationsDir,
|
|
77
88
|
});
|
|
78
|
-
const zodResult = generateZodTypes({
|
|
79
|
-
schemaFile: resolved.schemaFile,
|
|
80
|
-
outFile: resolved.zodFile,
|
|
81
|
-
});
|
|
82
|
-
return { migrate: migrateResult, codegen: codegenResult, zod: zodResult };
|
|
83
89
|
}
|
|
84
90
|
finally {
|
|
85
91
|
db.close();
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const { Client } = await import('pg');
|
|
93
|
-
const client = new Client({ connectionString: resolved.connectionString });
|
|
94
|
-
await client.connect();
|
|
94
|
+
else {
|
|
95
|
+
// Postgres
|
|
96
|
+
const introspector = new PostgresIntrospector(resolved.connectionString);
|
|
97
|
+
await introspector.connect();
|
|
95
98
|
try {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
const { Client } = await import('pg');
|
|
100
|
+
const client = new Client({ connectionString: resolved.connectionString });
|
|
101
|
+
await client.connect();
|
|
102
|
+
try {
|
|
103
|
+
const executor = new PostgresMigrationExecutor(client);
|
|
104
|
+
migrateResult = await migrate(executor, resolved.migrationsDir);
|
|
105
|
+
codegenResult = await generateSchemaTypes(introspector, {
|
|
106
|
+
outFile: resolved.schemaFile,
|
|
107
|
+
coercionFile: resolved.coercionFile,
|
|
108
|
+
manifestFile: resolved.manifestFile,
|
|
109
|
+
classificationMapFile: resolved.classificationMapFile,
|
|
110
|
+
camelCase: resolved.camelCase,
|
|
111
|
+
migrationsDir: resolved.migrationsDir,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await client.end();
|
|
116
|
+
}
|
|
110
117
|
}
|
|
111
118
|
finally {
|
|
112
|
-
await
|
|
119
|
+
await introspector.close();
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
const zodResult = generateZodTypes({
|
|
123
|
+
schemaFile: resolved.schemaFile,
|
|
124
|
+
outFile: resolved.zodFile,
|
|
125
|
+
});
|
|
126
|
+
// ── Classifications step ──────────────────────────────────────────────────
|
|
127
|
+
const { scaffolded, jsonWritten } = syncClassifications(resolved.classificationsFile, resolved.classificationsGenJsonFile, codegenResult.tables);
|
|
128
|
+
return {
|
|
129
|
+
migrate: migrateResult,
|
|
130
|
+
codegen: codegenResult,
|
|
131
|
+
zod: zodResult,
|
|
132
|
+
classificationsScaffolded: scaffolded,
|
|
133
|
+
classificationsJsonWritten: jsonWritten,
|
|
134
|
+
};
|
|
118
135
|
}
|
|
119
136
|
// ─── SQLite-only operations ───────────────────────────────────────────────────
|
|
120
137
|
export async function seed(resolved) {
|
|
@@ -139,6 +156,89 @@ export function reset(resolved, rootDir) {
|
|
|
139
156
|
rmSync(resolved.dbFile);
|
|
140
157
|
}
|
|
141
158
|
}
|
|
159
|
+
// ── Classification sync ───────────────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Loads `db/classifications.ts` via tsx, serialises it to
|
|
162
|
+
* `.pikku/db/classifications.gen.json` for runtime consumption (console, etc.).
|
|
163
|
+
*
|
|
164
|
+
* If `db/classifications.ts` doesn't exist yet, writes a scaffold with every
|
|
165
|
+
* table defaulting to `private` so the developer has a starting point.
|
|
166
|
+
*/
|
|
167
|
+
function syncClassifications(classificationsFile, genJsonFile, tableNames) {
|
|
168
|
+
let scaffolded = false;
|
|
169
|
+
if (!existsSync(classificationsFile)) {
|
|
170
|
+
const relMap = join(dirname(classificationsFile), '..', '.pikku', 'db', 'classification-map.gen.d.ts');
|
|
171
|
+
const relMapPosix = relMap.replace(/\\/g, '/');
|
|
172
|
+
const groups = new Map();
|
|
173
|
+
for (const name of tableNames) {
|
|
174
|
+
const dot = name.indexOf('.');
|
|
175
|
+
const schema = dot >= 0 ? name.slice(0, dot) : '';
|
|
176
|
+
const table = dot >= 0 ? name.slice(dot + 1) : name;
|
|
177
|
+
if (!groups.has(schema))
|
|
178
|
+
groups.set(schema, []);
|
|
179
|
+
groups.get(schema).push(table);
|
|
180
|
+
}
|
|
181
|
+
const bodyLines = [
|
|
182
|
+
`import type { DbClassificationMap } from '${relMapPosix}'`,
|
|
183
|
+
``,
|
|
184
|
+
`export const classifications = {`,
|
|
185
|
+
];
|
|
186
|
+
for (const [schema, tables] of groups) {
|
|
187
|
+
if (schema)
|
|
188
|
+
bodyLines.push(` ${JSON.stringify(schema)}: {`);
|
|
189
|
+
for (const table of tables) {
|
|
190
|
+
bodyLines.push(schema
|
|
191
|
+
? ` ${JSON.stringify(table)}: {`
|
|
192
|
+
: ` ${JSON.stringify(table)}: {`);
|
|
193
|
+
bodyLines.push(schema ? ` },` : ` },`);
|
|
194
|
+
}
|
|
195
|
+
if (schema)
|
|
196
|
+
bodyLines.push(` },`);
|
|
197
|
+
}
|
|
198
|
+
bodyLines.push(`} satisfies DbClassificationMap`, ``);
|
|
199
|
+
mkdirSync(dirname(classificationsFile), { recursive: true });
|
|
200
|
+
writeFileSync(classificationsFile, bodyLines.join('\n'), 'utf8');
|
|
201
|
+
scaffolded = true;
|
|
202
|
+
}
|
|
203
|
+
// Resolve tsx from the CLI package's own node_modules so it works regardless
|
|
204
|
+
// of whether the user's project has tsx installed.
|
|
205
|
+
const _require = createRequire(fileURLToPath(import.meta.url));
|
|
206
|
+
let tsxEsmPath = null;
|
|
207
|
+
try {
|
|
208
|
+
tsxEsmPath = _require.resolve('tsx/esm');
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// tsx not bundled with this CLI install — skip JSON emit
|
|
212
|
+
}
|
|
213
|
+
const script = [
|
|
214
|
+
`import * as mod from ${JSON.stringify(classificationsFile)}`,
|
|
215
|
+
`const val = Object.values(mod)[0]`,
|
|
216
|
+
`process.stdout.write(JSON.stringify(val))`,
|
|
217
|
+
].join('\n');
|
|
218
|
+
let jsonWritten = false;
|
|
219
|
+
if (tsxEsmPath) {
|
|
220
|
+
try {
|
|
221
|
+
const json = execSync(`node --import ${JSON.stringify(tsxEsmPath)} --input-type=module`, {
|
|
222
|
+
input: script,
|
|
223
|
+
encoding: 'utf8',
|
|
224
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
225
|
+
});
|
|
226
|
+
const existing = existsSync(genJsonFile)
|
|
227
|
+
? readFileSync(genJsonFile, 'utf8')
|
|
228
|
+
: null;
|
|
229
|
+
const next = JSON.stringify(JSON.parse(json), null, 2) + '\n';
|
|
230
|
+
if (existing !== next) {
|
|
231
|
+
mkdirSync(dirname(genJsonFile), { recursive: true });
|
|
232
|
+
writeFileSync(genJsonFile, next, 'utf8');
|
|
233
|
+
jsonWritten = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// annotations file has syntax errors — skip JSON emit
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { scaffolded, jsonWritten };
|
|
241
|
+
}
|
|
142
242
|
export async function createKysely(resolved) {
|
|
143
243
|
mkdirSync(dirname(resolved.dbFile), { recursive: true });
|
|
144
244
|
const runtime = await loadSqliteRuntime();
|