@pikku/cli 0.12.24 → 0.12.26
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-Ba9K10XZ.js +232 -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 +21 -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-meta.gen.json +50 -0
- 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 +183 -104
- 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 +3 -1
- package/dist/.pikku/pikku-services.gen.js +2 -0
- 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 +13 -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 +17 -5
- package/dist/.pikku/schemas/schemas/DbAuditInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuEmailsOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuFunctionTypesSplitInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuTriggerTypesInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/WorkspaceValidateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/WorkspaceValidateOutput.schema.json +1 -0
- 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 +5 -5
- 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/cli.wiring.js +39 -0
- package/dist/src/fabric/functions/validate-core.d.ts +20 -0
- package/dist/src/fabric/functions/validate-core.js +227 -0
- package/dist/src/fabric/functions/validate.function.js +12 -4
- package/dist/src/functions/commands/bootstrap.js +2 -2
- package/dist/src/functions/commands/console.js +7 -4
- package/dist/src/functions/commands/db-audit.d.ts +1 -0
- package/dist/src/functions/commands/db-audit.js +67 -0
- package/dist/src/functions/commands/db-migrate.js +7 -11
- package/dist/src/functions/commands/db-reset.js +12 -12
- package/dist/src/functions/commands/db-seed.js +11 -11
- package/dist/src/functions/commands/db-shared.d.ts +4 -19
- package/dist/src/functions/commands/db-shared.js +53 -17
- package/dist/src/functions/commands/dev.js +25 -14
- package/dist/src/functions/commands/emails-init.d.ts +5 -0
- package/dist/src/functions/commands/emails-init.js +162 -0
- package/dist/src/functions/commands/load-user-project.js +12 -3
- package/dist/src/functions/commands/new-addon.js +2 -2
- package/dist/src/functions/commands/tests-coverage.d.ts +3 -0
- package/dist/src/functions/commands/tests-coverage.js +34 -0
- package/dist/src/functions/commands/watch.js +7 -4
- package/dist/src/functions/commands/workspace-validate.d.ts +33 -0
- package/dist/src/functions/commands/workspace-validate.js +9 -0
- package/dist/src/functions/db/annotation-parser.d.ts +31 -0
- package/dist/src/functions/db/annotation-parser.js +93 -0
- package/dist/src/functions/db/coercion-plugin.d.ts +7 -0
- package/dist/src/functions/db/coercion-plugin.js +99 -0
- package/dist/src/functions/db/db-codegen.d.ts +24 -0
- package/dist/src/functions/db/db-codegen.js +276 -0
- package/dist/src/functions/db/db-introspector.d.ts +15 -0
- package/dist/src/functions/db/db-introspector.js +1 -0
- package/dist/src/functions/db/db-migrator.d.ts +32 -0
- package/dist/src/functions/db/db-migrator.js +65 -0
- package/dist/src/functions/db/local-db.d.ts +27 -33
- package/dist/src/functions/db/local-db.js +108 -57
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +10 -0
- package/dist/src/functions/db/postgres/postgres-introspector.js +54 -0
- package/dist/src/functions/db/postgres/postgres-migrator.d.ts +9 -0
- package/dist/src/functions/db/postgres/postgres-migrator.js +32 -0
- package/dist/src/functions/db/{seed.d.ts → sqlite/seed.d.ts} +2 -2
- package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +9 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +35 -0
- package/dist/src/functions/db/sqlite/sqlite-kysely.d.ts +8 -0
- package/dist/src/functions/db/sqlite/sqlite-kysely.js +62 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +10 -0
- package/dist/src/functions/db/sqlite/sqlite-migrator.js +36 -0
- package/dist/src/functions/db/sqlite/sqlite-runtime-bun.d.ts +2 -0
- package/dist/src/functions/db/sqlite/sqlite-runtime-bun.js +52 -0
- package/dist/src/functions/db/sqlite/sqlite-runtime-node.d.ts +2 -0
- package/dist/src/functions/db/sqlite/sqlite-runtime-node.js +51 -0
- package/dist/src/functions/db/sqlite/sqlite-runtime.d.ts +20 -0
- package/dist/src/functions/db/sqlite/sqlite-runtime.js +13 -0
- package/dist/src/functions/validate/workspace-validate.d.ts +34 -0
- package/dist/src/functions/validate/workspace-validate.js +259 -0
- package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +2 -1
- package/dist/src/functions/wirings/cli/pikku-command-cli-types.js +1 -1
- package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -4
- package/dist/src/functions/wirings/emails/pikku-command-emails.d.ts +6 -0
- package/dist/src/functions/wirings/emails/pikku-command-emails.js +172 -0
- package/dist/src/functions/wirings/emails/serialize-emails.d.ts +20 -0
- package/dist/src/functions/wirings/emails/serialize-emails.js +168 -0
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.d.ts +7 -1
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +2 -2
- package/dist/src/functions/wirings/functions/serialize-addon-types.js +1 -1
- package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.d.ts +7 -1
- package/dist/src/functions/wirings/triggers/pikku-command-trigger-types.js +2 -2
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +1 -1
- package/dist/src/functions/workflows/all.workflow.js +12 -7
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/services.js +2 -0
- package/dist/src/utils/pikku-cli-config.js +6 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -4
- package/skills/pikku-auth-js/SKILL.md +271 -58
- package/skills/pikku-testing/SKILL.md +208 -0
- package/console-app/assets/index-BDOqBctb.js +0 -232
- package/dist/src/functions/db/sql-migrator.d.ts +0 -26
- package/dist/src/functions/db/sql-migrator.js +0 -104
- package/dist/src/functions/db/sqlite-codegen.d.ts +0 -45
- package/dist/src/functions/db/sqlite-codegen.js +0 -294
- /package/dist/src/functions/db/{seed.js → sqlite/seed.js} +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
function fromDb(value, kind) {
|
|
2
|
+
if (value == null)
|
|
3
|
+
return value;
|
|
4
|
+
switch (kind) {
|
|
5
|
+
case 'date':
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
const d = new Date(value);
|
|
8
|
+
return Number.isNaN(d.getTime()) ? value : d;
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
case 'bool':
|
|
12
|
+
if (typeof value === 'number')
|
|
13
|
+
return value !== 0;
|
|
14
|
+
if (typeof value === 'bigint')
|
|
15
|
+
return value !== 0n;
|
|
16
|
+
return value;
|
|
17
|
+
case 'json':
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return value;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function snakeToCamel(name) {
|
|
29
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
function buildGlobalMap(map) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [table, tbl] of Object.entries(map)) {
|
|
34
|
+
for (const [col, kind] of Object.entries(tbl)) {
|
|
35
|
+
out[`${table}.${col}`] = kind;
|
|
36
|
+
out[`${table}.${snakeToCamel(col)}`] = kind;
|
|
37
|
+
out[col] = kind;
|
|
38
|
+
out[snakeToCamel(col)] = kind;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function collectQueryTables(node, out) {
|
|
44
|
+
if (!node || typeof node !== 'object')
|
|
45
|
+
return;
|
|
46
|
+
const op = node;
|
|
47
|
+
if (op.kind === 'TableNode') {
|
|
48
|
+
const tableName = op.table?.identifier?.name;
|
|
49
|
+
if (typeof tableName === 'string' && tableName.length > 0)
|
|
50
|
+
out.add(tableName);
|
|
51
|
+
}
|
|
52
|
+
for (const value of Object.values(node)) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
for (const item of value)
|
|
55
|
+
collectQueryTables(item, out);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
collectQueryTables(value, out);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function lookupKind(globalMap, tables, col) {
|
|
63
|
+
let matchedKind;
|
|
64
|
+
for (const table of tables) {
|
|
65
|
+
const kind = globalMap[`${table}.${col}`];
|
|
66
|
+
if (!kind)
|
|
67
|
+
continue;
|
|
68
|
+
if (matchedKind && matchedKind !== kind)
|
|
69
|
+
return globalMap[col];
|
|
70
|
+
matchedKind = kind;
|
|
71
|
+
}
|
|
72
|
+
return matchedKind ?? globalMap[col];
|
|
73
|
+
}
|
|
74
|
+
export function createCoercionPlugin(options) {
|
|
75
|
+
const globalMap = buildGlobalMap(options.map);
|
|
76
|
+
const queryTables = new WeakMap();
|
|
77
|
+
return {
|
|
78
|
+
transformQuery(args) {
|
|
79
|
+
const tables = new Set();
|
|
80
|
+
collectQueryTables(args.node, tables);
|
|
81
|
+
queryTables.set(args.queryId, [...tables]);
|
|
82
|
+
return args.node;
|
|
83
|
+
},
|
|
84
|
+
async transformResult(args) {
|
|
85
|
+
const tables = queryTables.get(args.queryId) ?? [];
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const row of args.result.rows) {
|
|
88
|
+
const next = { ...row };
|
|
89
|
+
for (const [col, val] of Object.entries(row)) {
|
|
90
|
+
const kind = lookupKind(globalMap, tables, col);
|
|
91
|
+
if (kind)
|
|
92
|
+
next[col] = fromDb(val, kind);
|
|
93
|
+
}
|
|
94
|
+
out.push(next);
|
|
95
|
+
}
|
|
96
|
+
return { ...args.result, rows: out };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DbIntrospector } from './db-introspector.js';
|
|
2
|
+
export interface CodegenOptions {
|
|
3
|
+
outFile: string;
|
|
4
|
+
coercionFile: string;
|
|
5
|
+
manifestFile?: string;
|
|
6
|
+
camelCase?: boolean;
|
|
7
|
+
migrationsDir?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CodegenResult {
|
|
10
|
+
outFile: string;
|
|
11
|
+
coercionFile: string;
|
|
12
|
+
manifestFile?: string;
|
|
13
|
+
written: boolean;
|
|
14
|
+
coercionWritten: boolean;
|
|
15
|
+
manifestWritten: boolean;
|
|
16
|
+
tables: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Introspect `introspector` and emit:
|
|
20
|
+
* - `schema.d.ts` Kysely DB type with classification brands
|
|
21
|
+
* - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
|
|
22
|
+
* - `classification.gen.ts` Data-classification manifest (when manifestFile set)
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateSchemaTypes(introspector: DbIntrospector, options: CodegenOptions): Promise<CodegenResult>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { parseAnnotations, annotationFromName, } from './annotation-parser.js';
|
|
4
|
+
// ─── Name helpers ─────────────────────────────────────────────────────────────
|
|
5
|
+
function snakeToPascal(name) {
|
|
6
|
+
return name
|
|
7
|
+
.split('_')
|
|
8
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
9
|
+
.join('');
|
|
10
|
+
}
|
|
11
|
+
function snakeToCamel(name) {
|
|
12
|
+
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
13
|
+
}
|
|
14
|
+
// ─── Type mapping ─────────────────────────────────────────────────────────────
|
|
15
|
+
function mapType(sqlType) {
|
|
16
|
+
const upper = sqlType.toUpperCase();
|
|
17
|
+
if (upper.includes('INT'))
|
|
18
|
+
return 'number';
|
|
19
|
+
if (upper.includes('CHAR') ||
|
|
20
|
+
upper.includes('CLOB') ||
|
|
21
|
+
upper.includes('TEXT') ||
|
|
22
|
+
upper === 'UUID')
|
|
23
|
+
return 'string';
|
|
24
|
+
if (upper.includes('BLOB') || upper === 'BYTEA')
|
|
25
|
+
return 'Buffer';
|
|
26
|
+
if (upper.includes('REAL') || upper.includes('FLOA') || upper.includes('DOUB'))
|
|
27
|
+
return 'number';
|
|
28
|
+
if (upper.includes('NUMERIC') || upper.includes('DECIMAL'))
|
|
29
|
+
return 'number';
|
|
30
|
+
// Postgres BOOLEAN type → boolean; SQLite BOOL (stores as int) → number
|
|
31
|
+
if (upper === 'BOOLEAN')
|
|
32
|
+
return 'boolean';
|
|
33
|
+
if (upper.includes('BOOL'))
|
|
34
|
+
return 'number';
|
|
35
|
+
if (upper.includes('JSON'))
|
|
36
|
+
return 'unknown';
|
|
37
|
+
return 'string';
|
|
38
|
+
}
|
|
39
|
+
// ─── Type expression ─────────────────────────────────────────────────────────
|
|
40
|
+
function selectBase(annotation, col) {
|
|
41
|
+
if (annotation?.kind === 'bool')
|
|
42
|
+
return 'boolean';
|
|
43
|
+
if (annotation?.kind === 'date')
|
|
44
|
+
return 'Date';
|
|
45
|
+
if (annotation?.kind === 'json')
|
|
46
|
+
return annotation.tsType ?? 'unknown';
|
|
47
|
+
return mapType(col.type);
|
|
48
|
+
}
|
|
49
|
+
function insertBase(annotation, col) {
|
|
50
|
+
if (annotation?.kind === 'bool')
|
|
51
|
+
return 'boolean | number';
|
|
52
|
+
if (annotation?.kind === 'date')
|
|
53
|
+
return 'Date | string';
|
|
54
|
+
if (annotation?.kind === 'json')
|
|
55
|
+
return annotation.tsType ?? 'unknown';
|
|
56
|
+
return mapType(col.type);
|
|
57
|
+
}
|
|
58
|
+
function columnTypeExpression(col, annotation, classification) {
|
|
59
|
+
const nullable = !col.notNull && !col.pk;
|
|
60
|
+
const isAutoInt = col.pk && mapType(col.type) === 'number';
|
|
61
|
+
const isOptionalInsert = col.defaultValue !== null || isAutoInt || Boolean(col.generated);
|
|
62
|
+
if (classification === 'public') {
|
|
63
|
+
const wrap = (inner) => isOptionalInsert ? `Generated<${inner}>` : inner;
|
|
64
|
+
if (annotation?.kind === 'bool') {
|
|
65
|
+
const base = nullable ? 'boolean | null' : 'boolean';
|
|
66
|
+
const rw = nullable ? 'boolean | number | null' : 'boolean | number';
|
|
67
|
+
return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
|
|
68
|
+
}
|
|
69
|
+
if (annotation?.kind === 'date') {
|
|
70
|
+
const base = nullable ? 'Date | null' : 'Date';
|
|
71
|
+
const rw = nullable ? 'Date | string | null' : 'Date | string';
|
|
72
|
+
return wrap(`ColumnType<${base}, ${rw}, ${rw}>`);
|
|
73
|
+
}
|
|
74
|
+
if (annotation?.kind === 'json') {
|
|
75
|
+
const base = annotation.tsType
|
|
76
|
+
? nullable
|
|
77
|
+
? `${annotation.tsType} | null`
|
|
78
|
+
: annotation.tsType
|
|
79
|
+
: nullable
|
|
80
|
+
? 'unknown | null'
|
|
81
|
+
: 'unknown';
|
|
82
|
+
return wrap(base);
|
|
83
|
+
}
|
|
84
|
+
const base = mapType(col.type);
|
|
85
|
+
if (isAutoInt)
|
|
86
|
+
return `Generated<${base}>`;
|
|
87
|
+
if (col.defaultValue !== null || col.generated)
|
|
88
|
+
return `Generated<${base}${nullable ? ' | null' : ''}>`;
|
|
89
|
+
return nullable ? `${base} | null` : base;
|
|
90
|
+
}
|
|
91
|
+
const B = classification === 'secret' ? 'Secret' : 'Private';
|
|
92
|
+
const sBase = selectBase(annotation, col);
|
|
93
|
+
const iBase = insertBase(annotation, col);
|
|
94
|
+
const selectT = nullable ? `${B}<${sBase}> | null` : `${B}<${sBase}>`;
|
|
95
|
+
const insertT = nullable
|
|
96
|
+
? `${iBase} | null${isOptionalInsert ? ' | undefined' : ''}`
|
|
97
|
+
: `${iBase}${isOptionalInsert ? ' | undefined' : ''}`;
|
|
98
|
+
const updateT = nullable ? `${iBase} | null` : iBase;
|
|
99
|
+
return `ColumnType<${selectT}, ${insertT}, ${updateT}>`;
|
|
100
|
+
}
|
|
101
|
+
function emitInterface(table, camelCase, explicitAnnotations) {
|
|
102
|
+
const ifaceName = snakeToPascal(table.name);
|
|
103
|
+
const tableCols = explicitAnnotations[table.name] ?? {};
|
|
104
|
+
const fields = table.columns
|
|
105
|
+
.map((col) => {
|
|
106
|
+
const fieldName = camelCase ? snakeToCamel(col.name) : col.name;
|
|
107
|
+
const sqlAnn = tableCols[col.name] ?? null;
|
|
108
|
+
const kindAnn = sqlAnn?.kind
|
|
109
|
+
? { kind: sqlAnn.kind, tsType: sqlAnn.tsType }
|
|
110
|
+
: annotationFromName(col.name);
|
|
111
|
+
const classification = sqlAnn?.classification ?? 'private';
|
|
112
|
+
const type = columnTypeExpression(col, kindAnn, classification);
|
|
113
|
+
const safeName = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName)
|
|
114
|
+
? fieldName
|
|
115
|
+
: JSON.stringify(fieldName);
|
|
116
|
+
return ` ${safeName}: ${type}`;
|
|
117
|
+
})
|
|
118
|
+
.join('\n');
|
|
119
|
+
return `export interface ${ifaceName} {\n${fields}\n}`;
|
|
120
|
+
}
|
|
121
|
+
// ─── Manifest emitter ────────────────────────────────────────────────────────
|
|
122
|
+
function emitManifest(tables, explicitAnnotations) {
|
|
123
|
+
const tableEntries = tables
|
|
124
|
+
.map((table) => {
|
|
125
|
+
const tableCols = explicitAnnotations[table.name] ?? {};
|
|
126
|
+
const colEntries = table.columns
|
|
127
|
+
.map((col) => {
|
|
128
|
+
const ann = tableCols[col.name];
|
|
129
|
+
const classification = ann?.classification ?? 'private';
|
|
130
|
+
const strategy = ann?.anonymize ?? null;
|
|
131
|
+
const strategyLiteral = strategy === null ? 'null' : `'${strategy}'`;
|
|
132
|
+
return (` ${JSON.stringify(col.name)}: ` +
|
|
133
|
+
`{ classification: '${classification}', anonymize_strategy: ${strategyLiteral} }`);
|
|
134
|
+
})
|
|
135
|
+
.join(',\n');
|
|
136
|
+
return ` ${JSON.stringify(table.name)}: {\n${colEntries}\n }`;
|
|
137
|
+
})
|
|
138
|
+
.join(',\n');
|
|
139
|
+
return [
|
|
140
|
+
`// Generated by @pikku/cli — do not edit by hand.`,
|
|
141
|
+
`// Run \`pikku db migrate\` to refresh.`,
|
|
142
|
+
``,
|
|
143
|
+
`export const classificationManifest = {`,
|
|
144
|
+
` version: 1 as const,`,
|
|
145
|
+
` tables: {`,
|
|
146
|
+
tableEntries,
|
|
147
|
+
` },`,
|
|
148
|
+
`} as const`,
|
|
149
|
+
``,
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Introspect `introspector` and emit:
|
|
154
|
+
* - `schema.d.ts` Kysely DB type with classification brands
|
|
155
|
+
* - `coercion.gen.ts` Runtime CoercionMap for date/bool/json coercion
|
|
156
|
+
* - `classification.gen.ts` Data-classification manifest (when manifestFile set)
|
|
157
|
+
*/
|
|
158
|
+
export async function generateSchemaTypes(introspector, options) {
|
|
159
|
+
const camelCase = options.camelCase ?? true;
|
|
160
|
+
const tableNames = await introspector.listTables();
|
|
161
|
+
const tables = await Promise.all(tableNames.map(async (name) => ({
|
|
162
|
+
name,
|
|
163
|
+
columns: await introspector.getColumns(name),
|
|
164
|
+
})));
|
|
165
|
+
const explicitAnnotations = options.migrationsDir
|
|
166
|
+
? parseAnnotations(options.migrationsDir)
|
|
167
|
+
: {};
|
|
168
|
+
// ── schema.d.ts ─────────────────────────────────────────────────────────────
|
|
169
|
+
const interfaces = tables
|
|
170
|
+
.map((t) => emitInterface(t, camelCase, explicitAnnotations))
|
|
171
|
+
.join('\n\n');
|
|
172
|
+
const dbEntries = tables
|
|
173
|
+
.map((t) => {
|
|
174
|
+
const tableKey = camelCase ? snakeToCamel(t.name) : t.name;
|
|
175
|
+
const safe = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableKey)
|
|
176
|
+
? tableKey
|
|
177
|
+
: JSON.stringify(tableKey);
|
|
178
|
+
return ` ${safe}: ${snakeToPascal(t.name)}`;
|
|
179
|
+
})
|
|
180
|
+
.join('\n');
|
|
181
|
+
const schemaBody = [
|
|
182
|
+
`// Generated by @pikku/cli — do not edit by hand.`,
|
|
183
|
+
`// Run \`pikku db migrate\` to refresh.`,
|
|
184
|
+
``,
|
|
185
|
+
`import type { ColumnType } from 'kysely'`,
|
|
186
|
+
``,
|
|
187
|
+
`export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>`,
|
|
188
|
+
` ? ColumnType<S, I | undefined, U>`,
|
|
189
|
+
` : ColumnType<T, T | undefined, T>`,
|
|
190
|
+
``,
|
|
191
|
+
`export type Private<T> = T & { readonly __pii__: 'private' }`,
|
|
192
|
+
`export type Secret<T> = T & { readonly __pii__: 'secret' }`,
|
|
193
|
+
``,
|
|
194
|
+
interfaces,
|
|
195
|
+
``,
|
|
196
|
+
`export interface DB {`,
|
|
197
|
+
dbEntries,
|
|
198
|
+
`}`,
|
|
199
|
+
``,
|
|
200
|
+
].join('\n');
|
|
201
|
+
// ── coercion.gen.ts ──────────────────────────────────────────────────────────
|
|
202
|
+
const coercionMap = {};
|
|
203
|
+
for (const table of tables) {
|
|
204
|
+
const tableCols = explicitAnnotations[table.name] ?? {};
|
|
205
|
+
for (const col of table.columns) {
|
|
206
|
+
const sqlAnn = tableCols[col.name];
|
|
207
|
+
const kind = sqlAnn?.kind ?? annotationFromName(col.name)?.kind;
|
|
208
|
+
if (kind) {
|
|
209
|
+
if (!coercionMap[table.name])
|
|
210
|
+
coercionMap[table.name] = {};
|
|
211
|
+
coercionMap[table.name][col.name] = kind;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const coercionEntries = Object.entries(coercionMap)
|
|
216
|
+
.map(([table, cols]) => {
|
|
217
|
+
const colEntries = Object.entries(cols)
|
|
218
|
+
.map(([col, kind]) => ` "${col}": "${kind}"`)
|
|
219
|
+
.join(',\n');
|
|
220
|
+
return ` "${table}": {\n${colEntries}\n }`;
|
|
221
|
+
})
|
|
222
|
+
.join(',\n');
|
|
223
|
+
const coercionBody = [
|
|
224
|
+
`// Generated by @pikku/cli — do not edit by hand.`,
|
|
225
|
+
`// Run \`pikku db migrate\` to refresh.`,
|
|
226
|
+
``,
|
|
227
|
+
`export const coercionMap = {`,
|
|
228
|
+
coercionEntries,
|
|
229
|
+
`} as const`,
|
|
230
|
+
``,
|
|
231
|
+
].join('\n');
|
|
232
|
+
// ── classification.gen.ts ───────────────────────────────────────────────────
|
|
233
|
+
const manifestBody = options.manifestFile ? emitManifest(tables, explicitAnnotations) : null;
|
|
234
|
+
// ── write files ───────────────────────────────────────────────────────────────
|
|
235
|
+
let existingSchema = null;
|
|
236
|
+
let existingCoercion = null;
|
|
237
|
+
let existingManifest = null;
|
|
238
|
+
try {
|
|
239
|
+
existingSchema = readFileSync(options.outFile, 'utf8');
|
|
240
|
+
}
|
|
241
|
+
catch { /* ok */ }
|
|
242
|
+
try {
|
|
243
|
+
existingCoercion = readFileSync(options.coercionFile, 'utf8');
|
|
244
|
+
}
|
|
245
|
+
catch { /* ok */ }
|
|
246
|
+
if (options.manifestFile) {
|
|
247
|
+
try {
|
|
248
|
+
existingManifest = readFileSync(options.manifestFile, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
catch { /* ok */ }
|
|
251
|
+
}
|
|
252
|
+
const schemaChanged = existingSchema !== schemaBody;
|
|
253
|
+
const coercionChanged = existingCoercion !== coercionBody;
|
|
254
|
+
const manifestChanged = manifestBody !== null && existingManifest !== manifestBody;
|
|
255
|
+
if (schemaChanged) {
|
|
256
|
+
mkdirSync(dirname(options.outFile), { recursive: true });
|
|
257
|
+
writeFileSync(options.outFile, schemaBody, 'utf8');
|
|
258
|
+
}
|
|
259
|
+
if (coercionChanged) {
|
|
260
|
+
mkdirSync(dirname(options.coercionFile), { recursive: true });
|
|
261
|
+
writeFileSync(options.coercionFile, coercionBody, 'utf8');
|
|
262
|
+
}
|
|
263
|
+
if (manifestChanged && options.manifestFile && manifestBody) {
|
|
264
|
+
mkdirSync(dirname(options.manifestFile), { recursive: true });
|
|
265
|
+
writeFileSync(options.manifestFile, manifestBody, 'utf8');
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
outFile: options.outFile,
|
|
269
|
+
coercionFile: options.coercionFile,
|
|
270
|
+
manifestFile: options.manifestFile,
|
|
271
|
+
written: schemaChanged,
|
|
272
|
+
coercionWritten: coercionChanged,
|
|
273
|
+
manifestWritten: manifestChanged,
|
|
274
|
+
tables: tables.map((t) => t.name),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ColumnInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
/** Raw SQL/DB type string, e.g. 'INTEGER', 'TEXT', 'boolean', 'timestamp without time zone' */
|
|
4
|
+
type: string;
|
|
5
|
+
notNull: boolean;
|
|
6
|
+
pk: boolean;
|
|
7
|
+
defaultValue: string | null;
|
|
8
|
+
/** True for virtual or stored generated columns — these are read-only and never inserted. */
|
|
9
|
+
generated?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface DbIntrospector {
|
|
12
|
+
listTables(): Promise<string[]>;
|
|
13
|
+
getColumns(table: string): Promise<ColumnInfo[]>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare class MigrationDriftError extends Error {
|
|
2
|
+
readonly file: string;
|
|
3
|
+
readonly recordedHash: string;
|
|
4
|
+
readonly currentHash: string | null;
|
|
5
|
+
readonly appliedAt: string;
|
|
6
|
+
constructor(file: string, recordedHash: string, currentHash: string | null, appliedAt: string, migrationsDir: string);
|
|
7
|
+
}
|
|
8
|
+
export interface MigrateResult {
|
|
9
|
+
applied: string[];
|
|
10
|
+
skipped: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface AppliedMigration {
|
|
13
|
+
name: string;
|
|
14
|
+
hash: string;
|
|
15
|
+
applied_at: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Provider-agnostic migration executor. Implement this for each DB dialect.
|
|
19
|
+
* Each method maps to a single DB operation; all file I/O and hashing lives
|
|
20
|
+
* in the shared `migrate()` function above.
|
|
21
|
+
*/
|
|
22
|
+
export interface MigrationExecutor {
|
|
23
|
+
ensureTrackingTable(): Promise<void>;
|
|
24
|
+
getApplied(): Promise<AppliedMigration[]>;
|
|
25
|
+
runMigration(sql: string, name: string, hash: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Apply pending migrations from `migrationsDir/*.sql` using the supplied
|
|
29
|
+
* executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
|
|
30
|
+
* with `MigrationDriftError` if any applied file has changed on disk.
|
|
31
|
+
*/
|
|
32
|
+
export declare function migrate(executor: MigrationExecutor, migrationsDir: string): Promise<MigrateResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export class MigrationDriftError extends Error {
|
|
5
|
+
file;
|
|
6
|
+
recordedHash;
|
|
7
|
+
currentHash;
|
|
8
|
+
appliedAt;
|
|
9
|
+
constructor(file, recordedHash, currentHash, appliedAt, migrationsDir) {
|
|
10
|
+
const onDisk = currentHash === null
|
|
11
|
+
? 'file missing on disk'
|
|
12
|
+
: `sha256:${currentHash.slice(0, 8)}…`;
|
|
13
|
+
super(`[PKU-DB-DRIFT] ${migrationsDir}/${file}\n\n` +
|
|
14
|
+
`Migration content has changed since it was applied.\n` +
|
|
15
|
+
` recorded: sha256:${recordedHash.slice(0, 8)}… applied ${appliedAt}\n` +
|
|
16
|
+
` on disk: ${onDisk}\n\n` +
|
|
17
|
+
`If this edit was intentional, write a new forward migration to revert the change.\n` +
|
|
18
|
+
`Production migrations are immutable.`);
|
|
19
|
+
this.file = file;
|
|
20
|
+
this.recordedHash = recordedHash;
|
|
21
|
+
this.currentHash = currentHash;
|
|
22
|
+
this.appliedAt = appliedAt;
|
|
23
|
+
this.name = 'MigrationDriftError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function sha256(bytes) {
|
|
27
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Apply pending migrations from `migrationsDir/*.sql` using the supplied
|
|
31
|
+
* executor. Hashes raw file bytes on apply; subsequent runs re-hash and bail
|
|
32
|
+
* with `MigrationDriftError` if any applied file has changed on disk.
|
|
33
|
+
*/
|
|
34
|
+
export async function migrate(executor, migrationsDir) {
|
|
35
|
+
await executor.ensureTrackingTable();
|
|
36
|
+
const applied = await executor.getApplied();
|
|
37
|
+
for (const row of applied) {
|
|
38
|
+
let currentHash = null;
|
|
39
|
+
try {
|
|
40
|
+
currentHash = sha256(readFileSync(join(migrationsDir, row.name)));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
currentHash = null;
|
|
44
|
+
}
|
|
45
|
+
if (currentHash !== row.hash) {
|
|
46
|
+
throw new MigrationDriftError(row.name, row.hash, currentHash, row.applied_at, migrationsDir);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const appliedSet = new Set(applied.map((r) => r.name));
|
|
50
|
+
const files = readdirSync(migrationsDir)
|
|
51
|
+
.filter((f) => f.endsWith('.sql'))
|
|
52
|
+
.sort();
|
|
53
|
+
const result = { applied: [], skipped: [] };
|
|
54
|
+
for (const name of files) {
|
|
55
|
+
if (appliedSet.has(name)) {
|
|
56
|
+
result.skipped.push(name);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const raw = readFileSync(join(migrationsDir, name));
|
|
60
|
+
const hash = sha256(raw);
|
|
61
|
+
await executor.runMigration(raw.toString('utf8'), name, hash);
|
|
62
|
+
result.applied.push(name);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
@@ -1,48 +1,42 @@
|
|
|
1
1
|
import type { Kysely } from 'kysely';
|
|
2
|
-
import { type MigrateResult } from './
|
|
3
|
-
import { type CodegenResult } from './
|
|
2
|
+
import { type MigrateResult } from './db-migrator.js';
|
|
3
|
+
import { type CodegenResult } from './db-codegen.js';
|
|
4
4
|
import { type ZodCodegenResult } from './zod-codegen.js';
|
|
5
|
-
import { type SeedResult } from './seed.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
export interface ResolvedLocalDb {
|
|
10
|
-
dbFile: string;
|
|
11
|
-
runtimeDir: string;
|
|
5
|
+
import { type SeedResult } from './sqlite/seed.js';
|
|
6
|
+
import type { UserConfigShape } from '../commands/db-shared.js';
|
|
7
|
+
interface ResolvedDbBase {
|
|
12
8
|
migrationsDir: string;
|
|
13
|
-
seedFile: string;
|
|
14
9
|
schemaFile: string;
|
|
15
10
|
coercionFile: string;
|
|
11
|
+
manifestFile: string;
|
|
16
12
|
zodFile: string;
|
|
17
13
|
camelCase: boolean;
|
|
18
14
|
}
|
|
15
|
+
export interface ResolvedSqliteDb extends ResolvedDbBase {
|
|
16
|
+
dialect: 'sqlite';
|
|
17
|
+
dbFile: string;
|
|
18
|
+
runtimeDir: string;
|
|
19
|
+
seedFile: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedPostgresDb extends ResolvedDbBase {
|
|
22
|
+
dialect: 'postgres';
|
|
23
|
+
connectionString: string;
|
|
24
|
+
}
|
|
25
|
+
export type ResolvedDb = ResolvedSqliteDb | ResolvedPostgresDb;
|
|
19
26
|
/**
|
|
20
|
-
* Resolve a
|
|
21
|
-
*
|
|
22
|
-
* - schema/coercion/zod are generated into outDir/db
|
|
23
|
-
* - migrations and seed are authored source under rootDir/db
|
|
27
|
+
* Resolve a UserConfigShape into an absolute-path descriptor.
|
|
28
|
+
* Returns null when neither sqliteDb nor postgresUrl is configured.
|
|
24
29
|
*/
|
|
25
|
-
export declare function
|
|
30
|
+
export declare function resolveDb(userConfig: UserConfigShape, rootDir: string, outDir: string, runtimeDir?: string): ResolvedDb | null;
|
|
31
|
+
/** @deprecated Use resolveDb(userConfig, ...) instead. */
|
|
32
|
+
export declare function resolveLocalDb(sqliteDb: string | undefined, rootDir: string, outDir: string, runtimeDir?: string): ResolvedSqliteDb | null;
|
|
26
33
|
export interface MigrateAndCodegenOutcome {
|
|
27
34
|
migrate: MigrateResult;
|
|
28
35
|
codegen: CodegenResult;
|
|
29
36
|
zod: ZodCodegenResult;
|
|
30
37
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
export declare function seed(resolved: ResolvedLocalDb): SeedResult;
|
|
37
|
-
/**
|
|
38
|
-
* Delete the dev DB file. Refuses if NODE_ENV is 'production' or the
|
|
39
|
-
* resolved file lives outside the project root (defensive against
|
|
40
|
-
* misconfigured absolute paths).
|
|
41
|
-
*/
|
|
42
|
-
export declare function reset(resolved: ResolvedLocalDb, rootDir: string): void;
|
|
43
|
-
/**
|
|
44
|
-
* Construct the user-facing Kysely instance for the dev DB. Used by
|
|
45
|
-
* `pikku dev` to populate inMemoryServices.kysely.
|
|
46
|
-
* Wires the coercion plugin when db/coercion.gen.ts exists.
|
|
47
|
-
*/
|
|
48
|
-
export declare function createKysely<DB>(resolved: ResolvedLocalDb): Promise<Kysely<DB>>;
|
|
38
|
+
export declare function migrateAndCodegen(resolved: ResolvedDb): Promise<MigrateAndCodegenOutcome>;
|
|
39
|
+
export declare function seed(resolved: ResolvedSqliteDb): Promise<SeedResult>;
|
|
40
|
+
export declare function reset(resolved: ResolvedSqliteDb, rootDir: string): void;
|
|
41
|
+
export declare function createKysely<DB>(resolved: ResolvedSqliteDb): Promise<Kysely<DB>>;
|
|
42
|
+
export {};
|