@pikku/cli 0.12.27 → 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-CQ29NRyR.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 +155 -133
- 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 +10 -9
- 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 +7 -7
- 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.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 +7 -1
- package/dist/src/functions/db/local-db.js +137 -37
- 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/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 +2 -2
- package/skills/pikku-services/SKILL.md +44 -7
- package/console-app/assets/index-BERGDBO9.js +0 -228
|
@@ -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,11 +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
|
-
seedFile: string;
|
|
10
10
|
schemaFile: string;
|
|
11
11
|
coercionFile: string;
|
|
12
12
|
manifestFile: string;
|
|
13
|
+
classificationMapFile: string;
|
|
14
|
+
classificationsFile: string;
|
|
15
|
+
classificationsGenJsonFile: string;
|
|
13
16
|
zodFile: string;
|
|
14
17
|
camelCase: boolean;
|
|
15
18
|
}
|
|
@@ -17,6 +20,7 @@ export interface ResolvedSqliteDb extends ResolvedDbBase {
|
|
|
17
20
|
dialect: 'sqlite';
|
|
18
21
|
dbFile: string;
|
|
19
22
|
runtimeDir: string;
|
|
23
|
+
seedFile: string;
|
|
20
24
|
}
|
|
21
25
|
export interface ResolvedPostgresDb extends ResolvedDbBase {
|
|
22
26
|
dialect: 'postgres';
|
|
@@ -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>;
|