URL ソース
capture-ledger の入力は Postgres のテーブル 1 つだけです。API は INSERT しません —
capture_targets への投入は呼び出し側の責務で、手動 INSERT でも、外部パイプラインでも、
同梱の seed でもかまいません。
開発用には、行を足す小さな CLI(pnpm run targets)も同梱しています(URL を追加する)。
fromTargets で始めたクロールが読むのは、これだけです。
SELECT url FROM capture_targets WHERE enabled AND org_id = $1 ORDER BY id ASC [LIMIT $2]ORDER BY id ASC なので登録順に起点になり、fromTargets.limit は先頭 n 件を取ります。
つまり動作確認では常に同じ URL が対象になります。
org_id で絞るのがテナントの境目です —— クロールは組織を 1 つしか持たないので、
別の組織の行を起点にすると帰属が言えなくなります。
.addColumn("url", "text", (col) => col.notNull().check(sql`url <> '' AND url = btrim(url)`)) .addColumn("url_hash", sql`bytea`, (col) => col.generatedAlwaysAs(sql`digest(url, 'sha256')`).stored(), ) .addColumn("labels", sql`text[]`, (col) => col.notNull().defaultTo(sql`'{}'::text[]`)) .addColumn("enabled", "boolean", (col) => col.notNull().defaultTo(true)) .addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)) .addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`))| カラム | 補足 |
|---|---|
id |
BIGSERIAL 主キー。投入順で、ローダの ORDER BY がそれを保つ。 |
url |
CHECK (url <> '' AND url = btrim(url)) と CHECK (url ~* '^https?://') —— 空文字・前後の空白・http(s) でない値をデータベースが拒否するので、呼ぶ側で検査する必要がない。 |
url_hash |
生成列 digest(url, 'sha256') (pgcrypto) を stored 保存。ユニークインデックスの土台で、直接読むことはない。 |
labels |
TEXT[]。もう誰も読みません —— 下記を参照。 |
enabled |
ホットパスは WHERE enabled で、部分インデックス capture_targets_enabled_id_idx が覆う。無効行はコストにならない。 |
org_id |
どの組織の対象か。既定値は無い —— INSERT では必ず書く(書かなければ NOT NULL で落ちる)。fromTargets はこれで絞る。 |
created_at / updated_at |
now() 既定。自動更新トリガは今のところ無い。 |
capture_targets_org_url_hash_key は (org_id, url_hash) のユニーク索引です。
1 つの組織の中では、同じ URL を二重に登録できません。
別々の組織なら、同じ URL をそれぞれの対象にできます。
labels の使い方
Section titled “labels の使い方”かつては自由形式で成果物のファイル名に入り、外部キーを持たせる場所として自然でした。
「自由形式」は文字どおりで、ファイル名の構造とぶつかる文字(_ . / 空白)は
BrowserHive が逃がすため、非 ASCII も含めてそのまま往復し、制限は「成果物の名前全体が
255 UTF-8 バイトに収まること」だけでした。同梱のサンプルは今も証券コードと社名を
並べています。
{ url: "https://www.ana.co.jp/group/", labels: ["9202", "ANAHoldings"] }URL を追加する
Section titled “URL を追加する”開発中は CLI を使います。DB に直接書く道具で、API は通らず、認可もありません。 どの組織の対象かは、必ず訊かれます。
pnpm run targets add https://example.com/ --org acmepnpm run targets add https://example.com/ https://example.org/docs --org acme --label demopnpm run targets add - --org acme < my-urls.txt # 1 行 1 URL。空行と # の行は読み飛ばすpnpm run targets list --org acme # --json で機械向けpnpm run targets disable 6 # 履歴を残したまま外す(enable 6 で戻す)pnpm run targets rm 6URL は、クロールが起点を読むのと同じ関数で読み、同じ形(フラグメントを落とした形)で保存します。
ここで足せた URL は、撮るときにも読めます。
読めない URL が 1 本でもあれば、何も足さずに名指しします。
無効にしてあった URL を足すと、有効に戻ります。
札は --label a --label b のように 1 つずつ渡します。
--label a b と書くと、b は URL として読まれ、読めないので何も足さずに止まります。
SQL で足すときは、組織を自分で書きます。
INSERT INTO capture_targets (url, org_id, labels) VALUES ('https://example.com/', 'acme', ARRAY['example']), ('https://example.org/', 'acme', ARRAY['example', 'org'])ON CONFLICT (org_id, url_hash) DO NOTHING;org_id は必ず書きます。既定値が無いので、書かなければ NOT NULL で断られます。
クロールは、呼んだ人の組織の行だけを起点にします。
履歴を残したまま対象から外したいときは、行を削除せずに無効にします
(pnpm run targets disable <id>、または enabled = false)。
マイグレーション
Section titled “マイグレーション”マイグレーションは src/db/migrations/<NNN>-<説明>.ts に置き、up(db) /
down(db) を export します。ランナーは Kysely の Migrator +
FileMigrationProvider の薄いラッパで、適用済み ID を kysely_migration
テーブルで管理します (同時実行は kysely_migration_lock が守ります)。最新の状態で
pnpm run db:migrate を再実行しても何も起きません。
pnpm run db:migrate # 適用pnpm run db:migrate:down # 直前の 1 つを巻き戻す追加するときは:
- 次の連番を取る (例
002-add-priority.ts)。 - スキーマビルダで
up/downを実装する。ビルダで書けないもの (拡張、生成列、 他カラムを参照するCHECK) はsql`…`.execute(db)を使う。 - ローカルで往復させる:
pnpm run db:migrate && pnpm run db:migrate:down && pnpm run db:migrate。 CI も同じ往復を実行します。 - マイグレーションと、それに依存するコードは同じ PR でコミットする。
seed も src/db/seeds/ に同じ形で置きますが、記録は kysely_seed と別なので、
マイグレーションとは独立に適用・巻き戻しできます。