feat(database): add revisioned knowledge model and seed catalog

This commit is contained in:
Christian Johannesen 2026-10-18 16:05:50 +02:00
parent 454ae1c915
commit 44c332c0df
15 changed files with 532 additions and 0 deletions

View file

@ -0,0 +1,11 @@
{
"name": "@tech-library/contracts",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
"scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run src" },
"dependencies": { "zod": "^4.1.5" },
"devDependencies": { "typescript": "^5.9.2", "vitest": "^5.0.5" }
}

View file

@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';
import { topicRevisionInputSchema } from './index.js';
describe('topicRevisionInputSchema', () => {
it('rejects invalid slugs and incomplete layered content', () => {
expect(() => topicRevisionInputSchema.parse({ slug: 'Not valid' })).toThrow();
});
});

119
packages/contracts/src/index.d.ts vendored Normal file
View file

@ -0,0 +1,119 @@
import { z } from 'zod';
export declare const topicTypes: readonly ["technology", "framework", "library", "language", "runtime", "tool", "platform", "standard", "practice", "architecture"];
export declare const relationTypes: readonly ["uses", "depends_on", "alternative_to", "integrates_with", "supersedes", "related_to"];
export declare const sourceTypes: readonly ["documentation", "changelog", "release_feed", "standards_body", "official_repository"];
export declare const topicSectionSchema: z.ZodObject<{
introduction: z.ZodString;
concepts: z.ZodArray<z.ZodString>;
useCases: z.ZodArray<z.ZodString>;
benefits: z.ZodArray<z.ZodString>;
limitations: z.ZodArray<z.ZodString>;
avoidWhen: z.ZodArray<z.ZodString>;
compatibility: z.ZodString;
requirements: z.ZodArray<z.ZodString>;
examples: z.ZodArray<z.ZodObject<{
title: z.ZodString;
language: z.ZodString;
code: z.ZodString;
}, z.core.$strip>>;
migrationNotes: z.ZodArray<z.ZodString>;
deprecations: z.ZodArray<z.ZodString>;
securityNotes: z.ZodArray<z.ZodString>;
}, z.core.$strip>;
export declare const topicRevisionInputSchema: z.ZodObject<{
title: z.ZodString;
slug: z.ZodString;
summary: z.ZodString;
audience: z.ZodArray<z.ZodEnum<{
beginner: "beginner";
intermediate: "intermediate";
advanced: "advanced";
}>>;
type: z.ZodEnum<{
technology: "technology";
framework: "framework";
library: "library";
language: "language";
runtime: "runtime";
tool: "tool";
platform: "platform";
standard: "standard";
practice: "practice";
architecture: "architecture";
}>;
domains: z.ZodArray<z.ZodString>;
tags: z.ZodArray<z.ZodString>;
aliases: z.ZodArray<z.ZodString>;
currentVersion: z.ZodNullable<z.ZodString>;
supportedVersions: z.ZodArray<z.ZodString>;
content: z.ZodObject<{
introduction: z.ZodString;
concepts: z.ZodArray<z.ZodString>;
useCases: z.ZodArray<z.ZodString>;
benefits: z.ZodArray<z.ZodString>;
limitations: z.ZodArray<z.ZodString>;
avoidWhen: z.ZodArray<z.ZodString>;
compatibility: z.ZodString;
requirements: z.ZodArray<z.ZodString>;
examples: z.ZodArray<z.ZodObject<{
title: z.ZodString;
language: z.ZodString;
code: z.ZodString;
}, z.core.$strip>>;
migrationNotes: z.ZodArray<z.ZodString>;
deprecations: z.ZodArray<z.ZodString>;
securityNotes: z.ZodArray<z.ZodString>;
}, z.core.$strip>;
changeNote: z.ZodString;
}, z.core.$strip>;
export type TopicRevisionInput = z.infer<typeof topicRevisionInputSchema>;
export type TopicSection = z.infer<typeof topicSectionSchema>;
export type TopicType = (typeof topicTypes)[number];
export type RelationType = (typeof relationTypes)[number];
export type SourceType = (typeof sourceTypes)[number];
export interface TopicListItem {
id: string;
slug: string;
title: string;
summary: string;
type: TopicType;
domains: string[];
tags: string[];
currentVersion: string | null;
verifiedAt: string;
}
export interface TopicDetail extends TopicListItem {
aliases: string[];
audience: string[];
supportedVersions: string[];
content: TopicSection;
revisionId: string;
revisionNumber: number;
relations: Array<{
type: RelationType;
topic: TopicListItem;
}>;
sources: Array<{
id: string;
title: string;
url: string;
publisher: string;
type: SourceType;
verifiedAt: string;
claims: string[];
}>;
}
export interface Paginated<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
export declare const findingStatusSchema: z.ZodEnum<{
approved: "approved";
rejected: "rejected";
pending: "pending";
published: "published";
failed: "failed";
}>;
export type FindingStatus = z.infer<typeof findingStatusSchema>;

View file

@ -0,0 +1,34 @@
import { z } from 'zod';
export const topicTypes = ['technology', 'framework', 'library', 'language', 'runtime', 'tool', 'platform', 'standard', 'practice', 'architecture'];
export const relationTypes = ['uses', 'depends_on', 'alternative_to', 'integrates_with', 'supersedes', 'related_to'];
export const sourceTypes = ['documentation', 'changelog', 'release_feed', 'standards_body', 'official_repository'];
export const topicSectionSchema = z.object({
introduction: z.string().min(1),
concepts: z.array(z.string()),
useCases: z.array(z.string()),
benefits: z.array(z.string()),
limitations: z.array(z.string()),
avoidWhen: z.array(z.string()),
compatibility: z.string(),
requirements: z.array(z.string()),
examples: z.array(z.object({ title: z.string(), language: z.string(), code: z.string() })),
migrationNotes: z.array(z.string()),
deprecations: z.array(z.string()),
securityNotes: z.array(z.string())
});
export const topicRevisionInputSchema = z.object({
title: z.string().min(2).max(120),
slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
summary: z.string().min(20).max(500),
audience: z.array(z.enum(['beginner', 'intermediate', 'advanced'])).min(1),
type: z.enum(topicTypes),
domains: z.array(z.string()).min(1),
tags: z.array(z.string()),
aliases: z.array(z.string()),
currentVersion: z.string().nullable(),
supportedVersions: z.array(z.string()),
content: topicSectionSchema,
changeNote: z.string().min(3)
});
export const findingStatusSchema = z.enum(['pending', 'approved', 'rejected', 'published', 'failed']);
//# sourceMappingURL=index.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}

View file

@ -0,0 +1,69 @@
import { z } from 'zod';
export const topicTypes = ['technology', 'framework', 'library', 'language', 'runtime', 'tool', 'platform', 'standard', 'practice', 'architecture'] as const;
export const relationTypes = ['uses', 'depends_on', 'alternative_to', 'integrates_with', 'supersedes', 'related_to'] as const;
export const sourceTypes = ['documentation', 'changelog', 'release_feed', 'standards_body', 'official_repository'] as const;
export const topicSectionSchema = z.object({
introduction: z.string().min(1),
concepts: z.array(z.string()),
useCases: z.array(z.string()),
benefits: z.array(z.string()),
limitations: z.array(z.string()),
avoidWhen: z.array(z.string()),
compatibility: z.string(),
requirements: z.array(z.string()),
examples: z.array(z.object({ title: z.string(), language: z.string(), code: z.string() })),
migrationNotes: z.array(z.string()),
deprecations: z.array(z.string()),
securityNotes: z.array(z.string())
});
export const topicRevisionInputSchema = z.object({
title: z.string().min(2).max(120),
slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
summary: z.string().min(20).max(500),
audience: z.array(z.enum(['beginner', 'intermediate', 'advanced'])).min(1),
type: z.enum(topicTypes),
domains: z.array(z.string()).min(1),
tags: z.array(z.string()),
aliases: z.array(z.string()),
currentVersion: z.string().nullable(),
supportedVersions: z.array(z.string()),
content: topicSectionSchema,
changeNote: z.string().min(3)
});
export type TopicRevisionInput = z.infer<typeof topicRevisionInputSchema>;
export type TopicSection = z.infer<typeof topicSectionSchema>;
export type TopicType = (typeof topicTypes)[number];
export type RelationType = (typeof relationTypes)[number];
export type SourceType = (typeof sourceTypes)[number];
export interface TopicListItem {
id: string;
slug: string;
title: string;
summary: string;
type: TopicType;
domains: string[];
tags: string[];
currentVersion: string | null;
verifiedAt: string;
}
export interface TopicDetail extends TopicListItem {
aliases: string[];
audience: string[];
supportedVersions: string[];
content: TopicSection;
revisionId: string;
revisionNumber: number;
relations: Array<{ type: RelationType; topic: TopicListItem }>;
sources: Array<{ id: string; title: string; url: string; publisher: string; type: SourceType; verifiedAt: string; claims: string[] }>;
}
export interface Paginated<T> { items: T[]; total: number; page: number; pageSize: number }
export const findingStatusSchema = z.enum(['pending', 'approved', 'rejected', 'published', 'failed']);
export type FindingStatus = z.infer<typeof findingStatusSchema>;

View file

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"]
}

View file

@ -0,0 +1,26 @@
{
"name": "@tech-library/database",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run --passWithNoTests",
"db:migrate": "tsx src/cli/migrate.ts",
"db:seed": "tsx src/cli/seed.ts"
},
"dependencies": {
"@tech-library/contracts": "*",
"better-sqlite3": "^12.2.0",
"drizzle-orm": "^0.45.4"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"tsx": "^4.20.5",
"typescript": "^5.9.2",
"vitest": "^5.0.5"
}
}

View file

@ -0,0 +1,4 @@
import { openDatabase } from '../index.js';
const { sqlite } = openDatabase();
console.log('Database migrations applied.');
sqlite.close();

View file

@ -0,0 +1,5 @@
import { openDatabase, seedDatabase } from '../index.js';
const { sqlite } = openDatabase();
const inserted = seedDatabase(sqlite);
console.log(`Seed complete: ${inserted} topic(s) inserted.`);
sqlite.close();

View file

@ -0,0 +1,24 @@
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { migrate } from './migrations.js';
import * as schema from './schema.js';
export * from './schema.js';
export { seedDatabase } from './seed.js';
export interface DatabaseContext {
sqlite: Database.Database;
db: ReturnType<typeof drizzle>;
}
export function openDatabase(filename = process.env.DATABASE_URL ?? './data/tech-library.db'): DatabaseContext {
const absolute = filename === ':memory:' ? filename : resolve(filename);
if (absolute !== ':memory:') mkdirSync(dirname(absolute), { recursive: true });
const sqlite = new Database(absolute);
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
migrate(sqlite);
return { sqlite, db: drizzle(sqlite, { schema }) };
}

View file

@ -0,0 +1,30 @@
import type Database from 'better-sqlite3';
const migrations = [
`CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS topics (id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, active_revision_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS revisions (id TEXT PRIMARY KEY, topic_id TEXT NOT NULL REFERENCES topics(id), number INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('draft','approved','rejected')), payload TEXT NOT NULL, change_note TEXT NOT NULL, created_at TEXT NOT NULL, reviewed_at TEXT, published_at TEXT, UNIQUE(topic_id, number));
CREATE TABLE IF NOT EXISTS sources (id TEXT PRIMARY KEY, url TEXT NOT NULL, publisher TEXT NOT NULL, source_type TEXT NOT NULL, title TEXT NOT NULL, published_at TEXT, fetched_at TEXT NOT NULL, verified_at TEXT NOT NULL, fingerprint TEXT NOT NULL, claims TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS revision_sources (revision_id TEXT NOT NULL REFERENCES revisions(id), source_id TEXT NOT NULL REFERENCES sources(id), PRIMARY KEY(revision_id, source_id));
CREATE TABLE IF NOT EXISTS relations (id TEXT PRIMARY KEY, source_topic_id TEXT NOT NULL REFERENCES topics(id), target_topic_id TEXT NOT NULL REFERENCES topics(id), type TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(source_topic_id, target_topic_id, type));
CREATE TABLE IF NOT EXISTS findings (id TEXT PRIMARY KEY, adapter TEXT NOT NULL, topic_id TEXT REFERENCES topics(id), source_url TEXT NOT NULL, fingerprint TEXT NOT NULL UNIQUE, status TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, proposed_payload TEXT, raw_metadata TEXT NOT NULL, discovered_at TEXT NOT NULL, reviewed_at TEXT, error TEXT);
CREATE TABLE IF NOT EXISTS source_subscriptions (id TEXT PRIMARY KEY, topic_id TEXT REFERENCES topics(id), adapter TEXT NOT NULL, url TEXT NOT NULL UNIQUE, enabled INTEGER NOT NULL DEFAULT 1, last_fingerprint TEXT, last_checked_at TEXT, last_error TEXT);
CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, csrf_token TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS audit_log (id TEXT PRIMARY KEY, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, metadata TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS worker_jobs (id TEXT PRIMARY KEY, type TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, started_at TEXT, completed_at TEXT, result TEXT);
CREATE VIRTUAL TABLE IF NOT EXISTS topic_search USING fts5(topic_id UNINDEXED, title, summary, aliases, content, tags, tokenize='porter unicode61');
CREATE INDEX IF NOT EXISTS revisions_topic_id ON revisions(topic_id);
CREATE INDEX IF NOT EXISTS findings_status ON findings(status);
CREATE INDEX IF NOT EXISTS relations_source ON relations(source_topic_id);`
];
export function migrate(sqlite: Database.Database): void {
sqlite.exec('CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)');
const applied = sqlite.prepare('SELECT 1 FROM _migrations WHERE version = ?');
const record = sqlite.prepare('INSERT INTO _migrations(version, applied_at) VALUES (?, ?)');
migrations.forEach((sql, index) => {
const version = index + 1;
if (applied.get(version)) return;
sqlite.transaction(() => { sqlite.exec(sql); record.run(version, new Date().toISOString()); })();
});
}

View file

@ -0,0 +1,100 @@
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
export const topics = sqliteTable('topics', {
id: text('id').primaryKey(),
slug: text('slug').notNull().unique(),
activeRevisionId: text('active_revision_id'),
createdAt: text('created_at').notNull(),
updatedAt: text('updated_at').notNull()
});
export const revisions = sqliteTable('revisions', {
id: text('id').primaryKey(),
topicId: text('topic_id').notNull().references(() => topics.id),
number: integer('number').notNull(),
status: text('status', { enum: ['draft', 'approved', 'rejected'] }).notNull(),
payload: text('payload', { mode: 'json' }).notNull(),
changeNote: text('change_note').notNull(),
createdAt: text('created_at').notNull(),
reviewedAt: text('reviewed_at'),
publishedAt: text('published_at')
}, (table) => [uniqueIndex('revision_topic_number').on(table.topicId, table.number)]);
export const sources = sqliteTable('sources', {
id: text('id').primaryKey(),
url: text('url').notNull(),
publisher: text('publisher').notNull(),
sourceType: text('source_type').notNull(),
title: text('title').notNull(),
publishedAt: text('published_at'),
fetchedAt: text('fetched_at').notNull(),
verifiedAt: text('verified_at').notNull(),
fingerprint: text('fingerprint').notNull(),
claims: text('claims', { mode: 'json' }).notNull()
});
export const revisionSources = sqliteTable('revision_sources', {
revisionId: text('revision_id').notNull().references(() => revisions.id),
sourceId: text('source_id').notNull().references(() => sources.id)
});
export const relations = sqliteTable('relations', {
id: text('id').primaryKey(),
sourceTopicId: text('source_topic_id').notNull().references(() => topics.id),
targetTopicId: text('target_topic_id').notNull().references(() => topics.id),
type: text('type').notNull(),
createdAt: text('created_at').notNull()
});
export const findings = sqliteTable('findings', {
id: text('id').primaryKey(),
adapter: text('adapter').notNull(),
topicId: text('topic_id').references(() => topics.id),
sourceUrl: text('source_url').notNull(),
fingerprint: text('fingerprint').notNull().unique(),
status: text('status').notNull(),
title: text('title').notNull(),
summary: text('summary').notNull(),
proposedPayload: text('proposed_payload', { mode: 'json' }),
rawMetadata: text('raw_metadata', { mode: 'json' }).notNull(),
discoveredAt: text('discovered_at').notNull(),
reviewedAt: text('reviewed_at'),
error: text('error')
});
export const sourceSubscriptions = sqliteTable('source_subscriptions', {
id: text('id').primaryKey(),
topicId: text('topic_id').references(() => topics.id),
adapter: text('adapter').notNull(),
url: text('url').notNull().unique(),
enabled: integer('enabled', { mode: 'boolean' }).notNull(),
lastFingerprint: text('last_fingerprint'),
lastCheckedAt: text('last_checked_at'),
lastError: text('last_error')
});
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
csrfToken: text('csrf_token').notNull(),
expiresAt: text('expires_at').notNull(),
createdAt: text('created_at').notNull()
});
export const auditLog = sqliteTable('audit_log', {
id: text('id').primaryKey(),
action: text('action').notNull(),
entityType: text('entity_type').notNull(),
entityId: text('entity_id').notNull(),
metadata: text('metadata', { mode: 'json' }).notNull(),
createdAt: text('created_at').notNull()
});
export const workerJobs = sqliteTable('worker_jobs', {
id: text('id').primaryKey(),
type: text('type').notNull(),
status: text('status').notNull(),
createdAt: text('created_at').notNull(),
startedAt: text('started_at'),
completedAt: text('completed_at'),
result: text('result', { mode: 'json' })
});

View file

@ -0,0 +1,91 @@
import type Database from 'better-sqlite3';
import { createHash, randomUUID } from 'node:crypto';
import type { TopicRevisionInput, TopicType } from '@tech-library/contracts';
type SeedTopic = { slug: string; title: string; summary: string; type: TopicType; domain: string; url: string; publisher: string; version?: string };
export const seedTopics: SeedTopic[] = [
{ slug: 'html', title: 'HTML', summary: 'The semantic markup language that gives web documents their structure and meaning.', type: 'language', domain: 'Web platform', url: 'https://html.spec.whatwg.org/', publisher: 'WHATWG' },
{ slug: 'css', title: 'CSS', summary: 'The styling language for responsive layouts, visual design, animation, and presentation on the web.', type: 'language', domain: 'Web platform', url: 'https://www.w3.org/Style/CSS/', publisher: 'W3C' },
{ slug: 'javascript', title: 'JavaScript', summary: 'The web platform programming language, standardized as ECMAScript and used across browsers and servers.', type: 'language', domain: 'Web platform', url: 'https://tc39.es/ecma262/', publisher: 'TC39' },
{ slug: 'typescript', title: 'TypeScript', summary: 'A statically typed language that extends JavaScript and compiles to standards-compatible JavaScript.', type: 'language', domain: 'Frontend and UI', url: 'https://www.typescriptlang.org/docs/', publisher: 'Microsoft' },
{ slug: 'web-components', title: 'Web Components', summary: 'Browser standards for reusable custom elements with encapsulated templates and styling.', type: 'standard', domain: 'Web platform', url: 'https://developer.mozilla.org/en-US/docs/Web/API/Web_components', publisher: 'Mozilla' },
{ slug: 'accessibility', title: 'Web Accessibility', summary: 'Practices and standards for making digital experiences perceivable, operable, understandable, and robust.', type: 'practice', domain: 'Web platform', url: 'https://www.w3.org/WAI/standards-guidelines/wcag/', publisher: 'W3C' },
{ slug: 'react', title: 'React', summary: 'A component-oriented library for building interactive user interfaces from declarative state.', type: 'library', domain: 'Frontend and UI', url: 'https://react.dev/', publisher: 'Meta' },
{ slug: 'vite', title: 'Vite', summary: 'A fast frontend development server and production build tool based on native ES modules and Rollup.', type: 'tool', domain: 'Frontend and UI', url: 'https://vite.dev/guide/', publisher: 'Vite' },
{ slug: 'pwa', title: 'Progressive Web Apps', summary: 'Installable web applications that use platform capabilities such as service workers and manifests.', type: 'platform', domain: 'Frontend and UI', url: 'https://web.dev/explore/progressive-web-apps', publisher: 'Google' },
{ slug: 'design-systems', title: 'Design Systems', summary: 'Shared foundations, components, patterns, and governance for consistent product interfaces.', type: 'practice', domain: 'Frontend and UI', url: 'https://www.w3.org/WAI/ARIA/apg/', publisher: 'W3C' },
{ slug: 'nodejs', title: 'Node.js', summary: 'A cross-platform JavaScript runtime for servers, command-line tools, automation, and build systems.', type: 'runtime', domain: 'Backend and APIs', url: 'https://nodejs.org/docs/latest/api/', publisher: 'OpenJS Foundation' },
{ slug: 'rest', title: 'REST', summary: 'An architectural style for networked systems centered on resources and uniform HTTP semantics.', type: 'architecture', domain: 'Backend and APIs', url: 'https://www.rfc-editor.org/rfc/rfc9110', publisher: 'IETF' },
{ slug: 'graphql', title: 'GraphQL', summary: 'A typed query language and execution model for APIs where clients select the data they need.', type: 'standard', domain: 'Backend and APIs', url: 'https://spec.graphql.org/', publisher: 'GraphQL Foundation' },
{ slug: 'openapi', title: 'OpenAPI', summary: 'A machine-readable specification for describing HTTP APIs, operations, schemas, and authentication.', type: 'standard', domain: 'Backend and APIs', url: 'https://spec.openapis.org/oas/latest.html', publisher: 'OpenAPI Initiative' },
{ slug: 'websockets', title: 'WebSocket', summary: 'A persistent, full-duplex protocol for bidirectional communication between clients and servers.', type: 'standard', domain: 'Backend and APIs', url: 'https://www.rfc-editor.org/rfc/rfc6455', publisher: 'IETF' },
{ slug: 'sqlite', title: 'SQLite', summary: 'An embedded transactional relational database stored in a portable single-file format.', type: 'technology', domain: 'Data', url: 'https://sqlite.org/docs.html', publisher: 'SQLite Consortium' },
{ slug: 'postgresql', title: 'PostgreSQL', summary: 'An extensible open-source relational database with strong transactional and standards support.', type: 'technology', domain: 'Data', url: 'https://www.postgresql.org/docs/', publisher: 'PostgreSQL Global Development Group' },
{ slug: 'database-indexing', title: 'Database Indexing', summary: 'Data structures and query-planning techniques that trade storage and write cost for faster reads.', type: 'practice', domain: 'Data', url: 'https://www.postgresql.org/docs/current/indexes.html', publisher: 'PostgreSQL Global Development Group' },
{ slug: 'data-migrations', title: 'Database Migrations', summary: 'Version-controlled, repeatable changes that evolve database schemas and data safely over time.', type: 'practice', domain: 'Data', url: 'https://www.sqlite.org/lang_altertable.html', publisher: 'SQLite Consortium' },
{ slug: 'react-native', title: 'React Native', summary: 'A React renderer for creating native mobile applications using JavaScript and platform components.', type: 'framework', domain: 'Mobile and desktop', url: 'https://reactnative.dev/docs/getting-started', publisher: 'Meta' },
{ slug: 'electron', title: 'Electron', summary: 'A framework for desktop applications that combines Chromium, Node.js, and web technologies.', type: 'framework', domain: 'Mobile and desktop', url: 'https://www.electronjs.org/docs/latest/', publisher: 'OpenJS Foundation' },
{ slug: 'tauri', title: 'Tauri', summary: 'A toolkit for small desktop and mobile applications using a system webview and a Rust backend.', type: 'framework', domain: 'Mobile and desktop', url: 'https://v2.tauri.app/', publisher: 'Tauri Programme' },
{ slug: 'docker', title: 'Docker', summary: 'A platform for packaging and running applications as isolated, reproducible containers.', type: 'platform', domain: 'DevOps and cloud', url: 'https://docs.docker.com/', publisher: 'Docker' },
{ slug: 'kubernetes', title: 'Kubernetes', summary: 'A declarative orchestration platform for deploying, scaling, and operating container workloads.', type: 'platform', domain: 'DevOps and cloud', url: 'https://kubernetes.io/docs/', publisher: 'CNCF' },
{ slug: 'github-actions', title: 'GitHub Actions', summary: 'Repository-integrated automation for continuous integration, delivery, and maintenance workflows.', type: 'platform', domain: 'DevOps and cloud', url: 'https://docs.github.com/actions', publisher: 'GitHub' },
{ slug: 'infrastructure-as-code', title: 'Infrastructure as Code', summary: 'Managing infrastructure through versioned, reviewable, and repeatable declarative definitions.', type: 'practice', domain: 'DevOps and cloud', url: 'https://developer.hashicorp.com/terraform/docs', publisher: 'HashiCorp' },
{ slug: 'automated-testing', title: 'Automated Testing', summary: 'Repeatable checks at multiple levels that provide confidence in software behavior and change.', type: 'practice', domain: 'Testing and observability', url: 'https://vitest.dev/guide/', publisher: 'Vitest' },
{ slug: 'observability', title: 'Observability', summary: 'Understanding system behavior through correlated telemetry such as logs, metrics, and traces.', type: 'practice', domain: 'Testing and observability', url: 'https://opentelemetry.io/docs/', publisher: 'CNCF' },
{ slug: 'web-performance', title: 'Web Performance', summary: 'Measuring and improving loading, responsiveness, rendering stability, and resource efficiency.', type: 'practice', domain: 'Testing and observability', url: 'https://web.dev/explore/learn-performance', publisher: 'Google' },
{ slug: 'owasp-top-ten', title: 'OWASP Top 10', summary: 'A widely used awareness document describing major security risks to modern web applications.', type: 'standard', domain: 'Security and privacy', url: 'https://owasp.org/www-project-top-ten/', publisher: 'OWASP' },
{ slug: 'oauth-2', title: 'OAuth 2.0', summary: 'An authorization framework for delegated access to protected HTTP resources.', type: 'standard', domain: 'Security and privacy', url: 'https://www.rfc-editor.org/rfc/rfc6749', publisher: 'IETF' },
{ slug: 'content-security-policy', title: 'Content Security Policy', summary: 'A browser security standard that restricts resource loading and mitigates injection attacks.', type: 'standard', domain: 'Security and privacy', url: 'https://www.w3.org/TR/CSP3/', publisher: 'W3C' }
];
function payload(topic: SeedTopic): TopicRevisionInput {
return {
title: topic.title, slug: topic.slug, summary: topic.summary,
audience: ['beginner', 'intermediate', 'advanced'], type: topic.type,
domains: [topic.domain], tags: [topic.domain.toLowerCase(), topic.type], aliases: [],
currentVersion: topic.version ?? null, supportedVersions: [],
content: {
introduction: `${topic.title} is a core part of modern software development. This entry provides a practical map of the technology, its trade-offs, and its official sources.`,
concepts: ['Core model and terminology', 'Ecosystem and interoperability', 'Lifecycle and maintenance'],
useCases: [`Evaluate ${topic.title} for new and existing systems`, 'Understand integration boundaries and operational impact'],
benefits: ['Backed by an official, traceable primary source', 'Presented in layers for readers with different experience levels'],
limitations: ['Exact capabilities and support vary by version and platform'],
avoidWhen: ['The operational cost or compatibility constraints exceed the value for the project'],
compatibility: 'Consult the linked official documentation for current platform and version compatibility.',
requirements: ['Verify supported versions before adoption', 'Test the intended production environment'],
examples: [], migrationNotes: [], deprecations: [],
securityNotes: ['Review the official security guidance and supported release policy before production use.']
},
changeNote: 'Initial curated catalog entry'
};
}
export function seedDatabase(sqlite: Database.Database): number {
const now = new Date().toISOString();
const insertTopic = sqlite.prepare('INSERT OR IGNORE INTO topics(id, slug, active_revision_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)');
const insertRevision = sqlite.prepare('INSERT OR IGNORE INTO revisions(id, topic_id, number, status, payload, change_note, created_at, reviewed_at, published_at) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?)');
const insertSource = sqlite.prepare('INSERT OR IGNORE INTO sources(id, url, publisher, source_type, title, fetched_at, verified_at, fingerprint, claims) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
const linkSource = sqlite.prepare('INSERT OR IGNORE INTO revision_sources(revision_id, source_id) VALUES (?, ?)');
const indexSearch = sqlite.prepare('INSERT INTO topic_search(topic_id, title, summary, aliases, content, tags) VALUES (?, ?, ?, ?, ?, ?)');
const countBefore = Number((sqlite.prepare('SELECT count(*) AS count FROM topics').get() as { count: number }).count);
sqlite.transaction(() => {
for (const item of seedTopics) {
const topicId = `topic_${item.slug}`;
const revisionId = `revision_${item.slug}_1`;
const sourceId = `source_${item.slug}_official`;
const data = payload(item);
insertTopic.run(topicId, item.slug, revisionId, now, now);
insertRevision.run(revisionId, topicId, 'approved', JSON.stringify(data), data.changeNote, now, now, now);
insertSource.run(sourceId, item.url, item.publisher, 'documentation', `${item.title} official documentation`, now, now, createHash('sha256').update(item.url).digest('hex'), JSON.stringify(['Official definition and current technical guidance']));
linkSource.run(revisionId, sourceId);
if (!(sqlite.prepare('SELECT 1 FROM topic_search WHERE topic_id = ?').get(topicId))) {
indexSearch.run(topicId, data.title, data.summary, data.aliases.join(' '), JSON.stringify(data.content), data.tags.join(' '));
}
sqlite.prepare('INSERT OR IGNORE INTO source_subscriptions(id, topic_id, adapter, url, enabled) VALUES (?, ?, ?, ?, 1)').run(randomUUID(), topicId, 'official-page', item.url);
}
})();
const countAfter = Number((sqlite.prepare('SELECT count(*) AS count FROM topics').get() as { count: number }).count);
return countAfter - countBefore;
}

View file

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src/**/*.ts"]
}