ktx/packages/context/src/scan/local-structural-artifacts.ts

126 lines
4.4 KiB
TypeScript
Raw Normal View History

2026-05-10 23:51:24 +02:00
import type { KtxLocalProject } from '../project/index.js';
2026-05-10 23:12:26 +02:00
import type {
2026-05-10 23:51:24 +02:00
KtxConnectionDriver,
KtxSchemaColumn,
KtxSchemaForeignKey,
KtxSchemaSnapshot,
KtxSchemaTable,
2026-05-10 23:12:26 +02:00
} from './types.js';
export interface ReadLocalScanStructuralSnapshotInput {
2026-05-10 23:51:24 +02:00
project: KtxLocalProject;
2026-05-10 23:12:26 +02:00
connectionId: string;
2026-05-10 23:51:24 +02:00
driver: KtxConnectionDriver;
2026-05-10 23:12:26 +02:00
rawSourcesDir: string;
extractedAtFallback: string;
}
interface LiveDatabaseConnectionArtifact {
connectionId?: unknown;
extractedAt?: unknown;
metadata?: unknown;
scope?: unknown;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function metadataRecord(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function optionalStringOrNull(value: unknown): string | null | undefined {
if (value === undefined) {
return undefined;
}
return typeof value === 'string' ? value : null;
}
2026-05-10 23:51:24 +02:00
function parseColumn(rawColumn: unknown, path: string): KtxSchemaColumn {
2026-05-10 23:12:26 +02:00
if (
!isRecord(rawColumn) ||
typeof rawColumn.name !== 'string' ||
typeof rawColumn.nativeType !== 'string' ||
typeof rawColumn.normalizedType !== 'string' ||
(rawColumn.dimensionType !== 'time' &&
rawColumn.dimensionType !== 'string' &&
rawColumn.dimensionType !== 'number' &&
rawColumn.dimensionType !== 'boolean')
) {
2026-05-10 23:51:24 +02:00
throw new Error(`Invalid KTX schema column artifact: ${path}`);
2026-05-10 23:12:26 +02:00
}
return {
name: rawColumn.name,
nativeType: rawColumn.nativeType,
normalizedType: rawColumn.normalizedType,
dimensionType: rawColumn.dimensionType,
nullable: rawColumn.nullable === true,
primaryKey: rawColumn.primaryKey === true,
comment: optionalStringOrNull(rawColumn.comment) ?? null,
};
}
2026-05-10 23:51:24 +02:00
function parseForeignKey(rawForeignKey: unknown, path: string): KtxSchemaForeignKey {
2026-05-10 23:12:26 +02:00
if (
!isRecord(rawForeignKey) ||
typeof rawForeignKey.fromColumn !== 'string' ||
typeof rawForeignKey.toTable !== 'string' ||
typeof rawForeignKey.toColumn !== 'string'
) {
2026-05-10 23:51:24 +02:00
throw new Error(`Invalid KTX schema foreign key artifact: ${path}`);
2026-05-10 23:12:26 +02:00
}
return {
fromColumn: rawForeignKey.fromColumn,
toCatalog: optionalStringOrNull(rawForeignKey.toCatalog) ?? null,
toDb: optionalStringOrNull(rawForeignKey.toDb) ?? null,
toTable: rawForeignKey.toTable,
toColumn: rawForeignKey.toColumn,
constraintName: optionalStringOrNull(rawForeignKey.constraintName) ?? null,
};
}
2026-05-10 23:51:24 +02:00
function parseTable(raw: string, path: string): KtxSchemaTable {
2026-05-10 23:12:26 +02:00
const parsed = JSON.parse(raw) as unknown;
if (!isRecord(parsed) || typeof parsed.name !== 'string' || !Array.isArray(parsed.columns)) {
2026-05-10 23:51:24 +02:00
throw new Error(`Invalid KTX schema table artifact: ${path}`);
2026-05-10 23:12:26 +02:00
}
return {
catalog: optionalStringOrNull(parsed.catalog) ?? null,
db: optionalStringOrNull(parsed.db) ?? null,
name: parsed.name,
kind:
parsed.kind === 'view' || parsed.kind === 'external' || parsed.kind === 'event_stream' ? parsed.kind : 'table',
comment: optionalStringOrNull(parsed.comment) ?? null,
estimatedRows: typeof parsed.estimatedRows === 'number' ? parsed.estimatedRows : null,
columns: parsed.columns.map((column) => parseColumn(column, path)),
foreignKeys: Array.isArray(parsed.foreignKeys)
? parsed.foreignKeys.map((foreignKey) => parseForeignKey(foreignKey, path))
: [],
};
}
export async function readLocalScanStructuralSnapshot(
input: ReadLocalScanStructuralSnapshotInput,
2026-05-10 23:51:24 +02:00
): Promise<KtxSchemaSnapshot> {
2026-05-10 23:12:26 +02:00
const connectionRaw = await input.project.fileStore.readFile(`${input.rawSourcesDir}/connection.json`);
const connection = JSON.parse(connectionRaw.content) as LiveDatabaseConnectionArtifact;
const listedTables = await input.project.fileStore.listFiles(`${input.rawSourcesDir}/tables`);
const tablePaths = listedTables.files.filter((path) => path.endsWith('.json')).sort();
2026-05-10 23:51:24 +02:00
const tables: KtxSchemaTable[] = [];
2026-05-10 23:12:26 +02:00
for (const path of tablePaths) {
const tableRaw = await input.project.fileStore.readFile(path);
tables.push(parseTable(tableRaw.content, path));
}
return {
connectionId: typeof connection.connectionId === 'string' ? connection.connectionId : input.connectionId,
driver: input.driver,
extractedAt: typeof connection.extractedAt === 'string' ? connection.extractedAt : input.extractedAtFallback,
scope: isRecord(connection.scope) ? connection.scope : {},
metadata: metadataRecord(connection.metadata),
tables,
};
}