UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

185 lines (172 loc) • 6.3 kB
#!/usr/bin/env node // Generates Kysely database types from the SQLite schema. import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import type { DatabaseSync } from "node:sqlite"; import { fileURLToPath } from "node:url"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; const SCHEMAS = ["openclaw-state", "openclaw-agent"]; const REPO_ROOT = fileURLToPath(new URL("../", import.meta.url)); function toInterfaceName(tableName: string): string { return tableName .split("_") .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) .join(""); } function columnBaseType(columnTypeLocal: string): string { const normalized = columnTypeLocal.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: Record<string, unknown>, primaryKeyColumnCount: number): string { const sqliteType = typeof column.type === "string" ? column.type : ""; const baseType = columnBaseType(sqliteType); const generatedPrimaryKey = primaryKeyColumnCount === 1 && Number(column.pk) > 0 && sqliteType.toUpperCase().includes("INT"); const generated = column.dflt_value != null || generatedPrimaryKey; const nullable = Number(column.notnull) !== 1 && !generatedPrimaryKey; const valueType = nullable ? `${baseType} | null` : baseType; return generated ? `Generated<${valueType}>` : valueType; } function quoteSqliteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } function generateTypes(db: DatabaseSync): string { const tables = db .prepare( "SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name;", ) .all() .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: Array<{ interfaceName: string; table: string }> = []; for (const table of tables) { const interfaceName = toInterfaceName(table); interfaces.push({ interfaceName, table }); lines.push(`export interface ${interfaceName} {`); const columns = db .prepare(`PRAGMA table_xinfo(${quoteSqliteIdentifier(table)});`) .all() .filter((column) => Number(column.hidden) === 0) .toSorted((left, right) => String(left.name).localeCompare(String(right.name), "en")); const primaryKeyColumnCount = columns.filter((column) => Number(column.pk) > 0).length; for (const column of columns) { lines.push(` ${String(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"); } async function generateKyselyTypes(schemaSource: string): Promise<string> { const { DatabaseSync } = await import("node:sqlite"); const db = new DatabaseSync(":memory:"); try { db.exec(schemaSource); return generateTypes(db); } finally { db.close(); } } export async function ensureKyselyTypes( cwd = REPO_ROOT, verify = false, options: { allowPartialCheckout?: boolean } = {}, ): Promise<void> { const outputDir = path.join(cwd, ".artifacts/kysely"); const stampFile = path.join(outputDir, "inputs.sha256"); const inputs = SCHEMAS.map((name) => ({ name, input: path.join(cwd, "src/state", `${name}-schema.sql`), output: path.join(outputDir, `${name}-db.generated.ts`), })); const available = inputs.filter((schema) => fs.existsSync(schema.input)); const partial = !verify && options.allowPartialCheckout === true; if (partial) { // Sparse lint must not consume a previous projection whose source is now omitted. for (const schema of inputs.filter((input) => !available.includes(input))) { fs.rmSync(schema.output, { force: true }); } } // Packaged installs and dependency-only Docker stages have no source schemas. if (!verify && available.length === 0) { if (partial) { fs.rmSync(stampFile, { force: true }); } return; } const schemas = (partial ? available : inputs).map((schema) => ({ name: schema.name, source: fs.readFileSync(schema.input, "utf8"), output: schema.output, })); const fingerprint = () => { const hash = createHash("sha256").update(fs.readFileSync(fileURLToPath(import.meta.url))); for (const schema of schemas) { hash.update(schema.name).update("\0").update(schema.source).update("\0"); hash.update(fs.existsSync(schema.output) ? fs.readFileSync(schema.output) : "missing"); } return hash.digest("hex"); }; if (!verify && fs.existsSync(stampFile) && fs.readFileSync(stampFile, "utf8") === fingerprint()) { return; } for (const schema of schemas) { const source = await generateKyselyTypes(schema.source); const existing = fs.existsSync(schema.output) ? fs.readFileSync(schema.output, "utf8") : undefined; if (verify) { if (existing !== source) { throw new Error(`${schema.output} is out of date. Run pnpm db:kysely:gen.`); } } else if (existing !== source) { fs.mkdirSync(outputDir, { recursive: true }); const temporary = `${schema.output}.${process.pid}.tmp`; fs.writeFileSync(temporary, source); fs.renameSync(temporary, schema.output); } } if (!verify) { fs.writeFileSync(stampFile, fingerprint()); } } if (isDirectRunUrl(process.argv[1], import.meta.url)) { await ensureKyselyTypes( REPO_ROOT, process.argv.includes("--verify") || process.argv.includes("--check"), ); }