Files
openclaw/scripts/generate-kysely-types.mjs
2026-05-28 00:46:32 +01:00

194 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
const SCHEMAS = [
{
name: "openclaw-state",
schema: "src/state/openclaw-state-schema.sql",
outFile: "src/state/openclaw-state-db.generated.d.ts",
schemaOutFile: "src/state/openclaw-state-schema.generated.ts",
schemaExport: "OPENCLAW_STATE_SCHEMA_SQL",
},
{
name: "openclaw-agent",
schema: "src/state/openclaw-agent-schema.sql",
outFile: "src/state/openclaw-agent-db.generated.d.ts",
schemaOutFile: "src/state/openclaw-agent-schema.generated.ts",
schemaExport: "OPENCLAW_AGENT_SCHEMA_SQL",
},
];
const verify = process.argv.includes("--verify") || process.argv.includes("--check");
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
stdio: options.input ? ["pipe", "inherit", "inherit"] : "inherit",
input: options.input,
encoding: "utf8",
env: { ...process.env, ...options.env },
cwd: options.cwd,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function runCapture(command, args, options = {}) {
const result = spawnSync(command, args, {
stdio: ["pipe", "pipe", "inherit"],
input: options.input,
encoding: "utf8",
env: { ...process.env, ...options.env },
cwd: options.cwd,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
return result.stdout;
}
function sqliteJson(dbPath, sql) {
const raw = runCapture("sqlite3", ["-json", dbPath, sql]);
return raw.trim() ? JSON.parse(raw) : [];
}
function toInterfaceName(tableName) {
return tableName
.split("_")
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
.join("");
}
function columnBaseType(columnType) {
const normalized = columnType.toUpperCase();
if (normalized.includes("BLOB")) {
return "Uint8Array";
}
if (
normalized.includes("INT") ||
normalized.includes("REAL") ||
normalized.includes("FLOA") ||
normalized.includes("DOUB") ||
normalized.includes("NUM") ||
normalized.includes("DEC")
) {
return "number";
}
return "string";
}
function columnType(column, primaryKeyColumnCount) {
const baseType = columnBaseType(String(column.type ?? ""));
const generated =
column.dflt_value != null ||
(primaryKeyColumnCount === 1 &&
Number(column.pk) > 0 &&
String(column.type ?? "")
.toUpperCase()
.includes("INT"));
const nullable = Number(column.notnull) !== 1 && !generated;
const valueType = nullable ? `${baseType} | null` : baseType;
return generated ? `Generated<${valueType}>` : valueType;
}
function generateTypes(dbPath) {
const tables = sqliteJson(
dbPath,
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name;",
).map((row) => String(row.name));
const lines = [
"/**",
" * This file was generated by kysely-codegen.",
" * Please do not edit it manually.",
" */",
"",
'import type { ColumnType } from "kysely";',
"",
"export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>",
" ? ColumnType<S, I | undefined, U>",
" : ColumnType<T, T | undefined, T>;",
"",
];
const interfaces = [];
for (const table of tables) {
const interfaceName = toInterfaceName(table);
interfaces.push({ interfaceName, table });
lines.push(`export interface ${interfaceName} {`);
const columns = sqliteJson(dbPath, `PRAGMA table_xinfo(${JSON.stringify(table)});`)
.filter((column) => Number(column.hidden) === 0)
.toSorted((left, right) => String(left.name).localeCompare(String(right.name)));
const primaryKeyColumnCount = columns.filter((column) => Number(column.pk) > 0).length;
for (const column of columns) {
lines.push(` ${column.name}: ${columnType(column, primaryKeyColumnCount)};`);
}
lines.push("}", "");
}
lines.push("export interface DB {");
for (const { interfaceName, table } of interfaces) {
lines.push(` ${table}: ${interfaceName};`);
}
lines.push("}", "");
return lines.join("\n");
}
function readUtf8(file) {
return fs.readFileSync(file, "utf8");
}
function generatedSchemaModule(schema) {
const source = readUtf8(schema.schema).trimEnd();
const literal = source.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${");
return [
"/**",
" * This file was generated from the SQLite schema source.",
" * Please do not edit it manually.",
" */",
"",
`export const ${schema.schemaExport} = \`${literal}\\n\`;`,
"",
].join("\n");
}
function generate(schema) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-kysely-${schema.name}-`));
const tmpDb = path.join(tmpDir, "schema.sqlite");
const tmpOut = verify ? path.join(tmpDir, "db.generated.d.ts") : schema.outFile;
const tmpSchemaOut = verify
? path.join(tmpDir, path.basename(schema.schemaOutFile))
: schema.schemaOutFile;
try {
run("sqlite3", [tmpDb], { input: readUtf8(schema.schema) });
fs.writeFileSync(tmpOut, generateTypes(tmpDb));
if (verify && readUtf8(tmpOut) !== readUtf8(schema.outFile)) {
console.error(`${schema.outFile} is out of date. Run pnpm db:kysely:gen.`);
process.exitCode = 1;
}
fs.writeFileSync(tmpSchemaOut, generatedSchemaModule(schema));
if (verify && readUtf8(tmpSchemaOut) !== readUtf8(schema.schemaOutFile)) {
console.error(`${schema.schemaOutFile} is out of date. Run pnpm db:kysely:gen.`);
process.exitCode = 1;
}
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
for (const schema of SCHEMAS) {
generate(schema);
}