test: define schema contract for topic fact graph
This commit is contained in:
34
src/core/schema.ts
Normal file
34
src/core/schema.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const TOPICS_TABLE = 'topics';
|
||||||
|
export const FACTS_TABLE = 'facts';
|
||||||
|
export const FACT_TOPICS_TABLE = 'fact_topics';
|
||||||
|
|
||||||
|
export const TOPIC_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'normalized_name',
|
||||||
|
'category',
|
||||||
|
'granularity',
|
||||||
|
'description',
|
||||||
|
'metadata',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const FACT_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'statement',
|
||||||
|
'summary',
|
||||||
|
'source',
|
||||||
|
'confidence',
|
||||||
|
'metadata',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const FACT_TOPIC_COLUMNS = [
|
||||||
|
'fact_id',
|
||||||
|
'topic_id',
|
||||||
|
'role',
|
||||||
|
'position',
|
||||||
|
'created_at',
|
||||||
|
] as const;
|
||||||
22
src/types/api.ts
Normal file
22
src/types/api.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { JsonValue, TopicCategory, TopicGranularity } from './domain';
|
||||||
|
|
||||||
|
export interface UpsertTopicInput {
|
||||||
|
name: string;
|
||||||
|
category?: TopicCategory;
|
||||||
|
granularity?: TopicGranularity;
|
||||||
|
description?: string | null;
|
||||||
|
metadata?: JsonValue | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TopicLinkInput extends UpsertTopicInput {
|
||||||
|
role?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddFactInput {
|
||||||
|
statement: string;
|
||||||
|
summary?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
confidence?: number | null;
|
||||||
|
metadata?: JsonValue | null;
|
||||||
|
topics: TopicLinkInput[];
|
||||||
|
}
|
||||||
7
src/types/database.ts
Normal file
7
src/types/database.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { FactRecord, FactTopicRecord, TopicRecord } from './domain';
|
||||||
|
|
||||||
|
export interface IdentityDatabaseSchema {
|
||||||
|
topics: TopicRecord;
|
||||||
|
facts: FactRecord;
|
||||||
|
fact_topics: FactTopicRecord;
|
||||||
|
}
|
||||||
37
src/types/domain.ts
Normal file
37
src/types/domain.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export type TopicCategory = 'entity' | 'concept' | 'temporal' | 'custom';
|
||||||
|
|
||||||
|
export type TopicGranularity = 'abstract' | 'concrete' | 'mixed';
|
||||||
|
|
||||||
|
export type JsonPrimitive = string | number | boolean | null;
|
||||||
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
export interface TopicRecord {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
normalized_name: string;
|
||||||
|
category: TopicCategory;
|
||||||
|
granularity: TopicGranularity;
|
||||||
|
description: string | null;
|
||||||
|
metadata: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactRecord {
|
||||||
|
id: string;
|
||||||
|
statement: string;
|
||||||
|
summary: string | null;
|
||||||
|
source: string | null;
|
||||||
|
confidence: number | null;
|
||||||
|
metadata: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactTopicRecord {
|
||||||
|
fact_id: string;
|
||||||
|
topic_id: string;
|
||||||
|
role: string | null;
|
||||||
|
position: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
88
tests/migrations.test.ts
Normal file
88
tests/migrations.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { sql } from 'kysely';
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { createDatabase } from '../src/adapters/dialect';
|
||||||
|
import { initializeSchema } from '../src/core/migrations';
|
||||||
|
|
||||||
|
const openConnections: Array<() => Promise<void>> = [];
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
while (openConnections.length > 0) {
|
||||||
|
const close = openConnections.pop();
|
||||||
|
if (close) {
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeSchema', () => {
|
||||||
|
it('creates the topics, facts, and fact_topics tables', async () => {
|
||||||
|
const connection = await createDatabase({ client: 'sqlite', filename: ':memory:' });
|
||||||
|
openConnections.push(connection.destroy);
|
||||||
|
|
||||||
|
await initializeSchema(connection.db);
|
||||||
|
|
||||||
|
const tables = await sql<{ name: string }>`
|
||||||
|
SELECT name
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'table'
|
||||||
|
ORDER BY name
|
||||||
|
`.execute(connection.db);
|
||||||
|
|
||||||
|
const tableNames = tables.rows.map((row) => row.name);
|
||||||
|
|
||||||
|
expect(tableNames).toContain('topics');
|
||||||
|
expect(tableNames).toContain('facts');
|
||||||
|
expect(tableNames).toContain('fact_topics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates the expected columns for each table', async () => {
|
||||||
|
const connection = await createDatabase({ client: 'sqlite', filename: ':memory:' });
|
||||||
|
openConnections.push(connection.destroy);
|
||||||
|
|
||||||
|
await initializeSchema(connection.db);
|
||||||
|
|
||||||
|
const topicsColumns = await sql<{ name: string }>`PRAGMA table_info(topics)`.execute(connection.db);
|
||||||
|
const factsColumns = await sql<{ name: string }>`PRAGMA table_info(facts)`.execute(connection.db);
|
||||||
|
const factTopicsColumns = await sql<{ name: string }>`PRAGMA table_info(fact_topics)`.execute(connection.db);
|
||||||
|
|
||||||
|
expect(topicsColumns.rows.map((row) => row.name)).toEqual([
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'normalized_name',
|
||||||
|
'category',
|
||||||
|
'granularity',
|
||||||
|
'description',
|
||||||
|
'metadata',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(factsColumns.rows.map((row) => row.name)).toEqual([
|
||||||
|
'id',
|
||||||
|
'statement',
|
||||||
|
'summary',
|
||||||
|
'source',
|
||||||
|
'confidence',
|
||||||
|
'metadata',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(factTopicsColumns.rows.map((row) => row.name)).toEqual([
|
||||||
|
'fact_id',
|
||||||
|
'topic_id',
|
||||||
|
'role',
|
||||||
|
'position',
|
||||||
|
'created_at',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent when called more than once', async () => {
|
||||||
|
const connection = await createDatabase({ client: 'sqlite', filename: ':memory:' });
|
||||||
|
openConnections.push(connection.destroy);
|
||||||
|
|
||||||
|
await initializeSchema(connection.db);
|
||||||
|
await expect(initializeSchema(connection.db)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user