Skip to content

Commit d77c3c3

Browse files
authored
Adding missing file (#61)
Signed-off-by: Denis Jannot <denis.jannot@solo.io>
1 parent 4ab0a65 commit d77c3c3

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

markdown-store.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Pool, PoolConfig } from 'pg';
2+
import { MarkdownStoreConfig } from './types';
3+
import { Logger } from './logger';
4+
5+
/**
6+
* Stores generated markdown for website pages in a Postgres table.
7+
*
8+
* Table schema (single shared table, default name `markdown_pages`):
9+
* url TEXT PRIMARY KEY
10+
* product_name TEXT NOT NULL
11+
* markdown TEXT NOT NULL
12+
* updated_at TIMESTAMPTZ DEFAULT NOW()
13+
*
14+
* When a website source has `markdown_store: true`, the crawl loop uses this
15+
* store to decide whether to force-process a page that would otherwise be
16+
* skipped by lastmod / ETag caching. If the URL is not yet in Postgres the
17+
* page is processed regardless of cache signals, ensuring the table is fully
18+
* populated after the first sync. On subsequent syncs only pages with
19+
* detected changes are updated.
20+
*/
21+
export class MarkdownStore {
22+
private pool: Pool;
23+
private tableName: string;
24+
private logger: Logger;
25+
26+
constructor(config: MarkdownStoreConfig, logger: Logger) {
27+
this.logger = logger.child('markdown-store');
28+
this.tableName = config.table_name ?? 'markdown_pages';
29+
30+
const poolConfig: PoolConfig = {};
31+
32+
if (config.connection_string) {
33+
poolConfig.connectionString = config.connection_string;
34+
} else {
35+
if (config.host) poolConfig.host = config.host;
36+
if (config.port) poolConfig.port = config.port;
37+
if (config.database) poolConfig.database = config.database;
38+
if (config.user) poolConfig.user = config.user;
39+
if (config.password) poolConfig.password = config.password;
40+
}
41+
42+
this.pool = new Pool(poolConfig);
43+
}
44+
45+
/**
46+
* Create the markdown table if it doesn't already exist.
47+
*/
48+
async init(): Promise<void> {
49+
const query = `
50+
CREATE TABLE IF NOT EXISTS ${this.escapeIdentifier(this.tableName)} (
51+
url TEXT PRIMARY KEY,
52+
product_name TEXT NOT NULL,
53+
markdown TEXT NOT NULL,
54+
updated_at TIMESTAMPTZ DEFAULT NOW()
55+
);
56+
`;
57+
await this.pool.query(query);
58+
this.logger.info(`Initialized Postgres markdown store (table: ${this.tableName})`);
59+
}
60+
61+
/**
62+
* Return the set of URLs that already have markdown stored for a given URL
63+
* prefix (e.g., "https://istio.io/latest/docs/"). This is called once
64+
* before the crawl starts so the crawler can decide which pages need
65+
* force-processing.
66+
*/
67+
async getUrlsWithMarkdown(urlPrefix: string): Promise<Set<string>> {
68+
const result = await this.pool.query(
69+
`SELECT url FROM ${this.escapeIdentifier(this.tableName)} WHERE url LIKE $1`,
70+
[urlPrefix + '%']
71+
);
72+
return new Set(result.rows.map((row: { url: string }) => row.url));
73+
}
74+
75+
/**
76+
* Insert or update the markdown for a URL. Called after a page is
77+
* successfully processed (fetched + converted to markdown).
78+
*/
79+
async upsertMarkdown(url: string, productName: string, markdown: string): Promise<void> {
80+
const query = `
81+
INSERT INTO ${this.escapeIdentifier(this.tableName)} (url, product_name, markdown, updated_at)
82+
VALUES ($1, $2, $3, NOW())
83+
ON CONFLICT (url) DO UPDATE SET
84+
product_name = EXCLUDED.product_name,
85+
markdown = EXCLUDED.markdown,
86+
updated_at = NOW();
87+
`;
88+
await this.pool.query(query, [url, productName, markdown]);
89+
}
90+
91+
/**
92+
* Remove a URL from the store (e.g., when a HEAD request returns 404).
93+
*/
94+
async deleteMarkdown(url: string): Promise<void> {
95+
await this.pool.query(
96+
`DELETE FROM ${this.escapeIdentifier(this.tableName)} WHERE url = $1`,
97+
[url]
98+
);
99+
}
100+
101+
/**
102+
* Close the connection pool. Should be called once after all sources have
103+
* been processed.
104+
*/
105+
async close(): Promise<void> {
106+
await this.pool.end();
107+
this.logger.info('Postgres markdown store connection pool closed');
108+
}
109+
110+
/**
111+
* Escape a SQL identifier (table name) to prevent injection.
112+
* Uses double-quoting per the SQL standard.
113+
*/
114+
private escapeIdentifier(identifier: string): string {
115+
return '"' + identifier.replace(/"/g, '""') + '"';
116+
}
117+
}

0 commit comments

Comments
 (0)