feat: add topic hierarchy APIs
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
type TopicWithFacts,
|
||||
type UpsertTopicInput,
|
||||
type AddFactInput,
|
||||
type LinkTopicsInput,
|
||||
} from '../types/api';
|
||||
import type { IngestStatementOptions } from '../ingestion/types';
|
||||
import type { DatabaseConnection, IdentityDBConnectionConfig } from '../adapters/dialect';
|
||||
@@ -35,6 +36,8 @@ import {
|
||||
findConnectedTopicRows,
|
||||
findTopicRowByNormalizedName,
|
||||
listTopicRows,
|
||||
findChildTopicRows,
|
||||
findParentTopicRows,
|
||||
type DatabaseExecutor,
|
||||
} from '../queries/topics';
|
||||
|
||||
@@ -152,6 +155,105 @@ export class IdentityDB {
|
||||
return this.addFact(factInput);
|
||||
}
|
||||
|
||||
async linkTopics(input: LinkTopicsInput): Promise<void> {
|
||||
const parentNormalizedName = normalizeTopicName(input.parentName);
|
||||
const childNormalizedName = normalizeTopicName(input.childName);
|
||||
|
||||
if (parentNormalizedName.length === 0 || childNormalizedName.length === 0) {
|
||||
throw new IdentityDBError('Topic hierarchy links require both a parent and child topic name.');
|
||||
}
|
||||
|
||||
if (parentNormalizedName === childNormalizedName) {
|
||||
throw new IdentityDBError('A topic cannot be linked as its own parent.');
|
||||
}
|
||||
|
||||
await this.connection.db.transaction().execute(async (trx) => {
|
||||
const parentTopic = await this.upsertTopicInExecutor(trx, {
|
||||
name: input.parentName,
|
||||
granularity: 'abstract',
|
||||
});
|
||||
const childTopic = await this.upsertTopicInExecutor(trx, {
|
||||
name: input.childName,
|
||||
});
|
||||
|
||||
const existing = await trx
|
||||
.selectFrom('topic_relations')
|
||||
.select(['parent_topic_id'])
|
||||
.where('parent_topic_id', '=', parentTopic.id)
|
||||
.where('child_topic_id', '=', childTopic.id)
|
||||
.where('relation', '=', 'parent_of')
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!existing) {
|
||||
await trx
|
||||
.insertInto('topic_relations')
|
||||
.values({
|
||||
parent_topic_id: parentTopic.id,
|
||||
child_topic_id: childTopic.id,
|
||||
relation: 'parent_of',
|
||||
created_at: nowIsoString(),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getTopicChildren(name: string): Promise<Topic[]> {
|
||||
const topicRow = await this.getRequiredTopicRow(name);
|
||||
|
||||
if (!topicRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const childRows = await findChildTopicRows(this.connection.db, topicRow.id);
|
||||
return childRows.map(mapTopicRow);
|
||||
}
|
||||
|
||||
async getTopicParents(name: string): Promise<Topic[]> {
|
||||
const topicRow = await this.getRequiredTopicRow(name);
|
||||
|
||||
if (!topicRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parentRows = await findParentTopicRows(this.connection.db, topicRow.id);
|
||||
return parentRows.map(mapTopicRow);
|
||||
}
|
||||
|
||||
async getTopicLineage(name: string): Promise<Topic[]> {
|
||||
const topicRow = await this.getRequiredTopicRow(name);
|
||||
|
||||
if (!topicRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lineage: Topic[] = [];
|
||||
const visitedTopicIds = new Set<string>([topicRow.id]);
|
||||
let currentLevelIds = [topicRow.id];
|
||||
|
||||
while (currentLevelIds.length > 0) {
|
||||
const nextLevelIds: string[] = [];
|
||||
|
||||
for (const currentId of currentLevelIds) {
|
||||
const parentRows = await findParentTopicRows(this.connection.db, currentId);
|
||||
|
||||
for (const parentRow of parentRows) {
|
||||
if (visitedTopicIds.has(parentRow.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visitedTopicIds.add(parentRow.id);
|
||||
nextLevelIds.push(parentRow.id);
|
||||
lineage.push(mapTopicRow(parentRow));
|
||||
}
|
||||
}
|
||||
|
||||
currentLevelIds = nextLevelIds;
|
||||
}
|
||||
|
||||
return lineage;
|
||||
}
|
||||
|
||||
async getTopicFacts(name: string): Promise<Fact[]> {
|
||||
const topicRow = await this.getRequiredTopicRow(name);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Kysely } from 'kysely';
|
||||
import {
|
||||
FACTS_TABLE,
|
||||
FACT_TOPICS_TABLE,
|
||||
TOPIC_RELATIONS_TABLE,
|
||||
TOPICS_TABLE,
|
||||
} from './schema';
|
||||
import type { IdentityDatabaseSchema } from '../types/database';
|
||||
@@ -52,6 +53,20 @@ export async function initializeSchema(
|
||||
.addPrimaryKeyConstraint('fact_topics_pk', ['fact_id', 'topic_id', 'position'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable(TOPIC_RELATIONS_TABLE)
|
||||
.ifNotExists()
|
||||
.addColumn('parent_topic_id', 'text', (column) =>
|
||||
column.notNull().references(`${TOPICS_TABLE}.id`).onDelete('cascade'),
|
||||
)
|
||||
.addColumn('child_topic_id', 'text', (column) =>
|
||||
column.notNull().references(`${TOPICS_TABLE}.id`).onDelete('cascade'),
|
||||
)
|
||||
.addColumn('relation', 'text', (column) => column.notNull())
|
||||
.addColumn('created_at', 'text', (column) => column.notNull())
|
||||
.addPrimaryKeyConstraint('topic_relations_pk', ['parent_topic_id', 'child_topic_id', 'relation'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('fact_topics_topic_id_idx')
|
||||
.ifNotExists()
|
||||
@@ -65,4 +80,18 @@ export async function initializeSchema(
|
||||
.on(FACT_TOPICS_TABLE)
|
||||
.column('fact_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('topic_relations_parent_topic_id_idx')
|
||||
.ifNotExists()
|
||||
.on(TOPIC_RELATIONS_TABLE)
|
||||
.column('parent_topic_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('topic_relations_child_topic_id_idx')
|
||||
.ifNotExists()
|
||||
.on(TOPIC_RELATIONS_TABLE)
|
||||
.column('child_topic_id')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const TOPICS_TABLE = 'topics';
|
||||
export const FACTS_TABLE = 'facts';
|
||||
export const FACT_TOPICS_TABLE = 'fact_topics';
|
||||
export const TOPIC_RELATIONS_TABLE = 'topic_relations';
|
||||
|
||||
export const TOPIC_COLUMNS = [
|
||||
'id',
|
||||
@@ -32,3 +33,10 @@ export const FACT_TOPIC_COLUMNS = [
|
||||
'position',
|
||||
'created_at',
|
||||
] as const;
|
||||
|
||||
export const TOPIC_RELATION_COLUMNS = [
|
||||
'parent_topic_id',
|
||||
'child_topic_id',
|
||||
'relation',
|
||||
'created_at',
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user