@notion-headless-cms/core 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,13 +75,14 @@ declare module "@notion-headless-cms/core" {
75
75
  - `list(opts?)` — 公開済み一覧(本文なし)
76
76
  - `params()` — SSG 用 slug 一覧
77
77
  - `cache.adjacent(slug, opts?)` — 前後記事ナビゲーション
78
- - `cache.warm(opts?)` / `cache.invalidate(scope?)` / `cache.checkItem(...)` — キャッシュ管理
78
+ - `cache.warm(opts?)` / `cache.prime(slug)` / `cache.invalidate(scope?)` / `cache.checkItem(...)` — キャッシュ管理(`prime` は単件ウォーム)
79
79
 
80
80
  ### グローバル操作
81
81
 
82
82
  - `cms.collections` — 登録コレクション名一覧
83
83
  - `cms.invalidate(scope?)` — `"all" | { collection } | { collection, slug }`
84
- - `cms.handler(opts?)` — Web Standard `(req) => Response`(画像プロキシ + webhook
84
+ - `cms.warmByPageId(pageId)` — Notion ページ ID から該当 1 件を単件ウォーム(公式 webhook 用)
85
+ - `cms.handler(opts?)` — Web Standard `(req) => Response`(画像プロキシ + webhook + `notion-webhook`)
85
86
  - `cms.getCachedImage(hash)` — 画像キャッシュ直アクセス
86
87
  - `cms.cacheImage(url)` — Notion 画像 URL を `{imageProxyBase}/{sha256}` 形式に変換し、永続キャッシュへ書き込む(画像キャッシュ未設定なら `undefined`)
87
88
  - `cms.imageProxyBase` — 画像プロキシのベース URL
@@ -1,4 +1,4 @@
1
- import { t as CacheAdapter } from "../cache-v9jTMnYd.mjs";
1
+ import { t as CacheAdapter } from "../cache-BnC6kxoU.mjs";
2
2
 
3
3
  //#region src/cache/memory.d.ts
4
4
  interface MemoryDocumentOptions {
@@ -16,13 +16,11 @@ var MemoryDocumentOps = class {
16
16
  metas = /* @__PURE__ */ new Map();
17
17
  contents = /* @__PURE__ */ new Map();
18
18
  maxItems;
19
- /** `cms.stats()` 用のヒット/ミス集計 (list+meta+content の合計)。 */
20
19
  hits = 0;
21
20
  misses = 0;
22
21
  constructor(options) {
23
22
  this.maxItems = options?.maxItems;
24
23
  }
25
- /** 保持エントリ数 (list / meta / content の合計)。stats() で使う。 */
26
24
  totalEntries() {
27
25
  return this.lists.size + this.metas.size + this.contents.size;
28
26
  }
@@ -108,13 +106,11 @@ var MemoryDocumentOps = class {
108
106
  }
109
107
  }
110
108
  };
111
- /** インメモリの画像オペレーション実装。 */
112
109
  var MemoryImageOps = class {
113
110
  store = /* @__PURE__ */ new Map();
114
111
  totalBytes = 0;
115
112
  maxItems;
116
113
  maxSizeBytes;
117
- /** `cms.stats()` 用のヒット/ミス集計。 */
118
114
  hits = 0;
119
115
  misses = 0;
120
116
  constructor(options) {
@@ -129,7 +125,6 @@ var MemoryImageOps = class {
129
125
  } else this.misses++;
130
126
  return Promise.resolve(entry ?? null);
131
127
  }
132
- /** stats() 用に、現在の保持エントリ数と合計バイト数を返す。 */
133
128
  snapshot() {
134
129
  return {
135
130
  entries: this.store.size,
@@ -1 +1 @@
1
- {"version":3,"file":"memory.mjs","names":[],"sources":["../../src/cache/memory.ts"],"sourcesContent":["import type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CacheAreaStats,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n InvalidateScope,\n StorageBinary,\n} from \"../types/index\";\n\nexport interface MemoryDocumentOptions {\n /** アイテム保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n}\n\nexport interface MemoryImageOptions {\n /** エントリ保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n /** 合計保持サイズ上限(バイト)。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxSizeBytes?: number;\n}\n\nexport interface MemoryCacheOptions\n extends MemoryDocumentOptions,\n MemoryImageOptions {}\n\n/**\n * Map の挿入順を LRU として扱う軽量実装。\n * `touch` で既存キーを末尾に移動、`enforceLimit` で古いキー(先頭)から削除する。\n */\nfunction touch<K, V>(map: Map<K, V>, key: K): void {\n const v = map.get(key);\n if (v === undefined) return;\n map.delete(key);\n map.set(key, v);\n}\n\nconst itemKey = (collection: string, slug: string): string =>\n `${collection}:${slug}`;\n\n/** インメモリのドキュメントオペレーション実装。プロセス再起動でクリアされる。 */\nclass MemoryDocumentOps implements DocumentCacheOps {\n private lists = new Map<string, CachedItemList<BaseContentItem>>();\n private metas = new Map<string, CachedItemMeta<BaseContentItem>>();\n private contents = new Map<string, CachedItemContent>();\n private readonly maxItems: number | undefined;\n /** `cms.stats()` 用のヒット/ミス集計 (list+meta+content の合計)。 */\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryDocumentOptions) {\n this.maxItems = options?.maxItems;\n }\n\n /** 保持エントリ数 (list / meta / content の合計)。stats() で使う。 */\n totalEntries(): number {\n return this.lists.size + this.metas.size + this.contents.size;\n }\n\n getList<T extends BaseContentItem>(\n collection: string,\n ): Promise<CachedItemList<T> | null> {\n const entry = this.lists.get(collection) as CachedItemList<T> | undefined;\n if (entry) this.hits++;\n else this.misses++;\n return Promise.resolve(entry ?? null);\n }\n\n setList<T extends BaseContentItem>(\n collection: string,\n data: CachedItemList<T>,\n ): Promise<void> {\n this.lists.set(collection, data);\n return Promise.resolve();\n }\n\n getMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n const key = itemKey(collection, slug);\n const entry = this.metas.get(key) as CachedItemMeta<T> | undefined;\n if (entry) {\n touch(this.metas, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n data: CachedItemMeta<T>,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.metas.has(key)) this.metas.delete(key);\n this.metas.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n getContent(\n collection: string,\n slug: string,\n ): Promise<CachedItemContent | null> {\n const key = itemKey(collection, slug);\n const entry = this.contents.get(key);\n if (entry) {\n touch(this.contents, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setContent(\n collection: string,\n slug: string,\n data: CachedItemContent,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.contents.has(key)) this.contents.delete(key);\n this.contents.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n invalidate(scope: InvalidateScope): Promise<void> {\n if (scope === \"all\") {\n this.lists.clear();\n this.metas.clear();\n this.contents.clear();\n return Promise.resolve();\n }\n const kind = scope.kind ?? \"all\";\n const collection = scope.collection;\n if (\"slug\" in scope) {\n const key = itemKey(collection, scope.slug);\n if (kind === \"all\" || kind === \"meta\") this.metas.delete(key);\n if (kind === \"all\" || kind === \"content\") this.contents.delete(key);\n // 単一スラッグ無効化ではリストは触らない(リスト全体の整合は別管理)\n return Promise.resolve();\n }\n // コレクション全体\n if (kind === \"all\" || kind === \"meta\") {\n this.lists.delete(collection);\n const prefix = `${collection}:`;\n for (const key of [...this.metas.keys()]) {\n if (key.startsWith(prefix)) this.metas.delete(key);\n }\n }\n if (kind === \"all\" || kind === \"content\") {\n const prefix = `${collection}:`;\n for (const key of [...this.contents.keys()]) {\n if (key.startsWith(prefix)) this.contents.delete(key);\n }\n }\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n if (this.maxItems === undefined) return;\n while (this.metas.size > this.maxItems) {\n const firstKey = this.metas.keys().next().value;\n if (firstKey === undefined) break;\n this.metas.delete(firstKey);\n }\n while (this.contents.size > this.maxItems) {\n const firstKey = this.contents.keys().next().value;\n if (firstKey === undefined) break;\n this.contents.delete(firstKey);\n }\n }\n}\n\n/** インメモリの画像オペレーション実装。 */\nclass MemoryImageOps implements ImageCacheOps {\n private store = new Map<string, StorageBinary>();\n private totalBytes = 0;\n private readonly maxItems: number | undefined;\n private readonly maxSizeBytes: number | undefined;\n /** `cms.stats()` 用のヒット/ミス集計。 */\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryImageOptions) {\n this.maxItems = options?.maxItems;\n this.maxSizeBytes = options?.maxSizeBytes;\n }\n\n get(hash: string): Promise<StorageBinary | null> {\n const entry = this.store.get(hash);\n if (entry) {\n touch(this.store, hash);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n /** stats() 用に、現在の保持エントリ数と合計バイト数を返す。 */\n snapshot(): { entries: number; sizeBytes: number } {\n return { entries: this.store.size, sizeBytes: this.totalBytes };\n }\n\n set(hash: string, data: ArrayBuffer, contentType: string): Promise<void> {\n const existing = this.store.get(hash);\n if (existing) {\n this.totalBytes -= existing.data.byteLength;\n this.store.delete(hash);\n }\n this.store.set(hash, { data, contentType });\n this.totalBytes += data.byteLength;\n this.enforceLimit();\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n while (\n (this.maxItems !== undefined && this.store.size > this.maxItems) ||\n (this.maxSizeBytes !== undefined && this.totalBytes > this.maxSizeBytes)\n ) {\n const firstKey = this.store.keys().next().value;\n if (firstKey === undefined) break;\n const victim = this.store.get(firstKey);\n if (victim) this.totalBytes -= victim.data.byteLength;\n this.store.delete(firstKey);\n }\n }\n}\n\n/**\n * インメモリのキャッシュアダプタ。document + image 両方を担当する。\n * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。\n *\n * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。\n *\n * @example\n * cache: [memoryCache({ maxItems: 1000 })]\n */\nexport function memoryCache(options?: MemoryCacheOptions): CacheAdapter {\n const doc = new MemoryDocumentOps(options);\n const img = new MemoryImageOps(options);\n return {\n name: \"memory\",\n handles: [\"document\", \"image\"] as const,\n doc,\n img,\n async stats(): Promise<CacheAdapterStats> {\n const imgSnap = img.snapshot();\n const docArea: CacheAreaStats = {\n hits: doc.hits,\n misses: doc.misses,\n entries: doc.totalEntries(),\n };\n const imgArea: CacheAreaStats = {\n hits: img.hits,\n misses: img.misses,\n entries: imgSnap.entries,\n sizeBytes: imgSnap.sizeBytes,\n };\n return { name: \"memory\", doc: docArea, img: imgArea };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"memory.mjs","names":[],"sources":["../../src/cache/memory.ts"],"sourcesContent":["import type {\n BaseContentItem,\n CacheAdapter,\n CacheAdapterStats,\n CacheAreaStats,\n CachedItemContent,\n CachedItemList,\n CachedItemMeta,\n DocumentCacheOps,\n ImageCacheOps,\n InvalidateScope,\n StorageBinary,\n} from \"../types/index\";\n\nexport interface MemoryDocumentOptions {\n /** アイテム保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n}\n\nexport interface MemoryImageOptions {\n /** エントリ保持上限。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxItems?: number;\n /** 合計保持サイズ上限(バイト)。未指定時は上限なし。超過時は LRU で古いものから削除。 */\n maxSizeBytes?: number;\n}\n\nexport interface MemoryCacheOptions\n extends MemoryDocumentOptions,\n MemoryImageOptions {}\n\n/**\n * Map の挿入順を LRU として扱う軽量実装。\n * `touch` で既存キーを末尾に移動、`enforceLimit` で古いキー(先頭)から削除する。\n */\nfunction touch<K, V>(map: Map<K, V>, key: K): void {\n const v = map.get(key);\n if (v === undefined) return;\n map.delete(key);\n map.set(key, v);\n}\n\nconst itemKey = (collection: string, slug: string): string =>\n `${collection}:${slug}`;\n\n/** インメモリのドキュメントオペレーション実装。プロセス再起動でクリアされる。 */\nclass MemoryDocumentOps implements DocumentCacheOps {\n private lists = new Map<string, CachedItemList<BaseContentItem>>();\n private metas = new Map<string, CachedItemMeta<BaseContentItem>>();\n private contents = new Map<string, CachedItemContent>();\n private readonly maxItems: number | undefined;\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryDocumentOptions) {\n this.maxItems = options?.maxItems;\n }\n\n totalEntries(): number {\n return this.lists.size + this.metas.size + this.contents.size;\n }\n\n getList<T extends BaseContentItem>(\n collection: string,\n ): Promise<CachedItemList<T> | null> {\n const entry = this.lists.get(collection) as CachedItemList<T> | undefined;\n if (entry) this.hits++;\n else this.misses++;\n return Promise.resolve(entry ?? null);\n }\n\n setList<T extends BaseContentItem>(\n collection: string,\n data: CachedItemList<T>,\n ): Promise<void> {\n this.lists.set(collection, data);\n return Promise.resolve();\n }\n\n getMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n ): Promise<CachedItemMeta<T> | null> {\n const key = itemKey(collection, slug);\n const entry = this.metas.get(key) as CachedItemMeta<T> | undefined;\n if (entry) {\n touch(this.metas, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setMeta<T extends BaseContentItem>(\n collection: string,\n slug: string,\n data: CachedItemMeta<T>,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.metas.has(key)) this.metas.delete(key);\n this.metas.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n getContent(\n collection: string,\n slug: string,\n ): Promise<CachedItemContent | null> {\n const key = itemKey(collection, slug);\n const entry = this.contents.get(key);\n if (entry) {\n touch(this.contents, key);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n setContent(\n collection: string,\n slug: string,\n data: CachedItemContent,\n ): Promise<void> {\n const key = itemKey(collection, slug);\n if (this.contents.has(key)) this.contents.delete(key);\n this.contents.set(key, data);\n this.enforceLimit();\n return Promise.resolve();\n }\n\n invalidate(scope: InvalidateScope): Promise<void> {\n if (scope === \"all\") {\n this.lists.clear();\n this.metas.clear();\n this.contents.clear();\n return Promise.resolve();\n }\n const kind = scope.kind ?? \"all\";\n const collection = scope.collection;\n if (\"slug\" in scope) {\n const key = itemKey(collection, scope.slug);\n if (kind === \"all\" || kind === \"meta\") this.metas.delete(key);\n if (kind === \"all\" || kind === \"content\") this.contents.delete(key);\n // 単一スラッグ無効化ではリストは触らない(リスト全体の整合は別管理)\n return Promise.resolve();\n }\n if (kind === \"all\" || kind === \"meta\") {\n this.lists.delete(collection);\n const prefix = `${collection}:`;\n for (const key of [...this.metas.keys()]) {\n if (key.startsWith(prefix)) this.metas.delete(key);\n }\n }\n if (kind === \"all\" || kind === \"content\") {\n const prefix = `${collection}:`;\n for (const key of [...this.contents.keys()]) {\n if (key.startsWith(prefix)) this.contents.delete(key);\n }\n }\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n if (this.maxItems === undefined) return;\n while (this.metas.size > this.maxItems) {\n const firstKey = this.metas.keys().next().value;\n if (firstKey === undefined) break;\n this.metas.delete(firstKey);\n }\n while (this.contents.size > this.maxItems) {\n const firstKey = this.contents.keys().next().value;\n if (firstKey === undefined) break;\n this.contents.delete(firstKey);\n }\n }\n}\n\nclass MemoryImageOps implements ImageCacheOps {\n private store = new Map<string, StorageBinary>();\n private totalBytes = 0;\n private readonly maxItems: number | undefined;\n private readonly maxSizeBytes: number | undefined;\n hits = 0;\n misses = 0;\n\n constructor(options?: MemoryImageOptions) {\n this.maxItems = options?.maxItems;\n this.maxSizeBytes = options?.maxSizeBytes;\n }\n\n get(hash: string): Promise<StorageBinary | null> {\n const entry = this.store.get(hash);\n if (entry) {\n touch(this.store, hash);\n this.hits++;\n } else {\n this.misses++;\n }\n return Promise.resolve(entry ?? null);\n }\n\n snapshot(): { entries: number; sizeBytes: number } {\n return { entries: this.store.size, sizeBytes: this.totalBytes };\n }\n\n set(hash: string, data: ArrayBuffer, contentType: string): Promise<void> {\n const existing = this.store.get(hash);\n if (existing) {\n this.totalBytes -= existing.data.byteLength;\n this.store.delete(hash);\n }\n this.store.set(hash, { data, contentType });\n this.totalBytes += data.byteLength;\n this.enforceLimit();\n return Promise.resolve();\n }\n\n private enforceLimit(): void {\n while (\n (this.maxItems !== undefined && this.store.size > this.maxItems) ||\n (this.maxSizeBytes !== undefined && this.totalBytes > this.maxSizeBytes)\n ) {\n const firstKey = this.store.keys().next().value;\n if (firstKey === undefined) break;\n const victim = this.store.get(firstKey);\n if (victim) this.totalBytes -= victim.data.byteLength;\n this.store.delete(firstKey);\n }\n }\n}\n\n/**\n * インメモリのキャッシュアダプタ。document + image 両方を担当する。\n * プロセス再起動でクリアされるため、ローカル開発・SSG ビルド・テスト用途。\n *\n * `stats()` でヒット率・エントリ数・画像合計バイト数を返す。\n *\n * @example\n * cache: [memoryCache({ maxItems: 1000 })]\n */\nexport function memoryCache(options?: MemoryCacheOptions): CacheAdapter {\n const doc = new MemoryDocumentOps(options);\n const img = new MemoryImageOps(options);\n return {\n name: \"memory\",\n handles: [\"document\", \"image\"] as const,\n doc,\n img,\n async stats(): Promise<CacheAdapterStats> {\n const imgSnap = img.snapshot();\n const docArea: CacheAreaStats = {\n hits: doc.hits,\n misses: doc.misses,\n entries: doc.totalEntries(),\n };\n const imgArea: CacheAreaStats = {\n hits: img.hits,\n misses: img.misses,\n entries: imgSnap.entries,\n sizeBytes: imgSnap.sizeBytes,\n };\n return { name: \"memory\", doc: docArea, img: imgArea };\n },\n };\n}\n"],"mappings":"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"}
@@ -69,6 +69,12 @@ interface DataSource<T extends BaseContentItem = BaseContentItem> {
69
69
  * Core が slug フィールドのルックアップに使用する。
70
70
  */
71
71
  findByProp?(notionPropName: string, value: string): Promise<T | null>;
72
+ /**
73
+ * Notion ページ ID で1件取得する(公式 webhook の sparse payload は page id のみを送るため、
74
+ * `warmByPageId` がこれで slug を解決する)。
75
+ * 自身の data source に属さないページを渡された場合は `null` を返す実装とする。
76
+ */
77
+ findById?(pageId: string): Promise<T | null>;
72
78
  /** アイテム本文を ContentBlock 配列で返す。 */
73
79
  loadBlocks(item: T): Promise<ContentBlock[]>;
74
80
  /** アイテム本文を Markdown 文字列で返す (html() 生成の元ソース)。 */
@@ -195,4 +201,4 @@ interface CacheAdapter {
195
201
  }
196
202
  //#endregion
197
203
  export { ImageCacheOps as a, InvalidateScope as c, WebhookConfig as d, DocumentCacheOps as i, PropertyDef as l, CacheAdapterStats as n, DataSource as o, CacheAreaStats as r, InvalidateKind as s, CacheAdapter as t, PropertyMap as u };
198
- //# sourceMappingURL=cache-v9jTMnYd.d.mts.map
204
+ //# sourceMappingURL=cache-BnC6kxoU.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { t as BaseContentItem } from "./content-DwsfWZao.mjs";
2
- import { o as DataSource, t as CacheAdapter } from "./cache-v9jTMnYd.mjs";
3
- import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BmrOz8T6.mjs";
2
+ import { o as DataSource, t as CacheAdapter } from "./cache-BnC6kxoU.mjs";
3
+ import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BPhaVO6-.mjs";
4
4
 
5
5
  //#region src/types/sources.d.ts
6
6
  /**
@@ -167,6 +167,13 @@ interface CreateClientOptions<S extends CMSSources = CMSSources> {
167
167
  imageProxyBase?: string;
168
168
  /** Cloudflare Workers の `waitUntil` に相当する非同期処理の登録関数。 */
169
169
  waitUntil?: (p: Promise<unknown>) => void;
170
+ /**
171
+ * Notion 公式 webhook(integration の Webhooks)の検証トークン(HMAC 署名キー)。
172
+ * 設定すると `cms.handler()` の `POST {basePath}/notion-webhook` が署名検証つきで有効になり、
173
+ * 受信イベントの対象ページを `warmByPageId` で再ウォームする。
174
+ * `cms.handler({ notionWebhook: { secret } })` で個別上書きも可能。
175
+ */
176
+ notionWebhookSecret?: string;
170
177
  /** ライフサイクルフック (全コレクション共通)。 */
171
178
  hooks?: CMSHooks<BaseContentItem>;
172
179
  /** プラグイン配列。 */
@@ -182,4 +189,4 @@ interface CreateClientOptions<S extends CMSSources = CMSSources> {
182
189
  }
183
190
  //#endregion
184
191
  export { MergeSourceCollections as _, DEFAULT_RATE_LIMITER as a, RateLimiterConfig as c, RendererPluginList as d, SWRConfig as f, CMSSources as g, CMSAdapter as h, CreateClientOptions as i, RenderOptions as l, defineCollection as m, CollectionsConfig as n, InferCollectionItem as o, StrictCollectionDef as p, ContentConfig as r, LogLevel as s, CollectionDef as t, RendererFn as u };
185
- //# sourceMappingURL=config-i99tKRhN.d.mts.map
192
+ //# sourceMappingURL=config-RnU3jrFb.d.mts.map
@@ -12,6 +12,7 @@
12
12
  * | `webhook/payload_invalid` | Webhook ペイロード形式不正 |
13
13
  * | `webhook/unknown_collection` | Webhook の対象コレクションが未知 |
14
14
  * | `webhook/not_implemented` | DataSource が parseWebhook を実装していない |
15
+ * | `handler/unknown_collection` | `cms.handler()` のルート (versions / check) が指す対象コレクションが未知 |
15
16
  * | `source/fetch_items_failed` | `DataSource.list()` 失敗 |
16
17
  * | `source/fetch_item_failed` | `DataSource.findByProp()` 失敗 |
17
18
  * | `source/load_markdown_failed` | `DataSource.loadMarkdown()` 失敗 |
@@ -34,7 +35,7 @@
34
35
  *
35
36
  * サードパーティアダプタが独自コードを追加したい場合は `CMSErrorCode` を参照。
36
37
  */
37
- type BuiltInCMSErrorCode = "core/config_invalid" | "core/schema_invalid" | "core/notion_orm_missing" | "core/sort_unsupported_type" | "webhook/signature_invalid" | "webhook/payload_invalid" | "webhook/unknown_collection" | "webhook/not_implemented" | "source/fetch_items_failed" | "source/fetch_item_failed" | "source/load_markdown_failed" | "source/load_blocks_failed" | "source/blocks_unsupported" | "cache/io_failed" | "cache/image_fetch_failed" | "cache/image_invalid_content_type" | "renderer/failed" | "swr/item_check_failed" | "swr/list_check_failed" | "swr/content_rebuild_failed" | "cli/config_invalid" | "cli/config_load_failed" | "cli/schema_invalid" | "cli/generate_failed" | "cli/init_failed" | "cli/notion_api_failed" | "cli/env_file_not_found";
38
+ type BuiltInCMSErrorCode = "core/config_invalid" | "core/schema_invalid" | "core/notion_orm_missing" | "core/sort_unsupported_type" | "webhook/signature_invalid" | "webhook/payload_invalid" | "webhook/unknown_collection" | "webhook/not_implemented" | "handler/unknown_collection" | "source/fetch_items_failed" | "source/fetch_item_failed" | "source/load_markdown_failed" | "source/load_blocks_failed" | "source/blocks_unsupported" | "cache/io_failed" | "cache/image_fetch_failed" | "cache/image_invalid_content_type" | "renderer/failed" | "swr/item_check_failed" | "swr/list_check_failed" | "swr/content_rebuild_failed" | "cli/config_invalid" | "cli/config_load_failed" | "cli/schema_invalid" | "cli/generate_failed" | "cli/init_failed" | "cli/notion_api_failed" | "cli/env_file_not_found";
38
39
  /**
39
40
  * CMS エラーコード。
40
41
  * `BuiltInCMSErrorCode` のリテラル補完を維持しつつ、
@@ -93,4 +94,4 @@ declare function matchCMSError<R>(error: unknown, handlers: Partial<Record<CMSEr
93
94
  }): R | undefined;
94
95
  //#endregion
95
96
  export { isCMSError as a, CMSErrorContext as i, CMSError as n, isCMSErrorInNamespace as o, CMSErrorCode as r, matchCMSError as s, BuiltInCMSErrorCode as t };
96
- //# sourceMappingURL=errors-DcNErfYk.d.mts.map
97
+ //# sourceMappingURL=errors-DPD5yq9S.d.mts.map
package/dist/errors.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as isCMSError, i as CMSErrorContext, n as CMSError, o as isCMSErrorInNamespace, r as CMSErrorCode, s as matchCMSError, t as BuiltInCMSErrorCode } from "./errors-DcNErfYk.mjs";
1
+ import { a as isCMSError, i as CMSErrorContext, n as CMSError, o as isCMSErrorInNamespace, r as CMSErrorCode, s as matchCMSError, t as BuiltInCMSErrorCode } from "./errors-DPD5yq9S.mjs";
2
2
  export { BuiltInCMSErrorCode, CMSError, CMSErrorCode, CMSErrorContext, isCMSError, isCMSErrorInNamespace, matchCMSError };
package/dist/errors.mjs CHANGED
@@ -39,6 +39,10 @@ const BUILT_IN_ERROR_HELP = {
39
39
  docsAnchor: "webhook-not_implemented",
40
40
  nextSteps: ["対象 collection の DataSource に parseWebhook を実装する", "Webhook を使わない場合は cms.invalidate() を直接呼ぶ運用に切り替える"]
41
41
  },
42
+ "handler/unknown_collection": {
43
+ docsAnchor: "handler-unknown_collection",
44
+ nextSteps: ["versions / check のルート URL が登録済みのコレクション名を指しているか確認する", "createCMS の collections に該当コレクションが定義されているか確認する"]
45
+ },
42
46
  "source/fetch_items_failed": {
43
47
  docsAnchor: "source-fetch_items_failed",
44
48
  nextSteps: [
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * ライブラリ組み込みの CMS エラーコード。\n *\n * | コード | 発生条件 |\n * |---|---|\n * | `core/config_invalid` | 設定不備(token 未設定など) |\n * | `core/schema_invalid` | schema/mapping の型不整合 |\n * | `core/notion_orm_missing` | `@notion-headless-cms/notion-orm` の動的ロード失敗 |\n * | `core/sort_unsupported_type` | ソートキーの値型が string / number でない |\n * | `webhook/signature_invalid` | Webhook 署名検証失敗 |\n * | `webhook/payload_invalid` | Webhook ペイロード形式不正 |\n * | `webhook/unknown_collection` | Webhook の対象コレクションが未知 |\n * | `webhook/not_implemented` | DataSource が parseWebhook を実装していない |\n * | `source/fetch_items_failed` | `DataSource.list()` 失敗 |\n * | `source/fetch_item_failed` | `DataSource.findByProp()` 失敗 |\n * | `source/load_markdown_failed` | `DataSource.loadMarkdown()` 失敗 |\n * | `source/load_blocks_failed` | `DataSource.loadBlocks()` 失敗 |\n * | `source/blocks_unsupported` | 選択した fetch 戦略が NotionBlockTree 取得を提供していない (markdown 戦略選択時など) |\n * | `cache/io_failed` | document / image キャッシュの I/O 失敗 |\n * | `cache/image_fetch_failed` | Notion 画像の HTTP 取得失敗 |\n * | `cache/image_invalid_content_type` | 画像レスポンスの Content-Type が不正 |\n * | `renderer/failed` | Markdown → HTML 変換失敗 |\n * | `swr/item_check_failed` | SWR バックグラウンドのアイテム差分チェック失敗 |\n * | `swr/list_check_failed` | SWR バックグラウンドのリスト差分チェック失敗 |\n * | `swr/content_rebuild_failed` | SWR バックグラウンドの本文再生成失敗 |\n * | `cli/config_invalid` | `nhc.config.ts` の内容不整合 |\n * | `cli/config_load_failed` | 設定ファイルの読み込み / 評価失敗 |\n * | `cli/schema_invalid` | CLI が受け取ったスキーマ / マッピング不整合 |\n * | `cli/generate_failed` | `nhc generate` の処理失敗 |\n * | `cli/init_failed` | `nhc init` の処理失敗 |\n * | `cli/notion_api_failed` | CLI が Notion API を呼び出す際の失敗 |\n * | `cli/env_file_not_found` | `--env-file` で指定したファイルが存在しない |\n *\n * サードパーティアダプタが独自コードを追加したい場合は `CMSErrorCode` を参照。\n */\nexport type BuiltInCMSErrorCode =\n | \"core/config_invalid\"\n | \"core/schema_invalid\"\n | \"core/notion_orm_missing\"\n | \"core/sort_unsupported_type\"\n | \"webhook/signature_invalid\"\n | \"webhook/payload_invalid\"\n | \"webhook/unknown_collection\"\n | \"webhook/not_implemented\"\n | \"source/fetch_items_failed\"\n | \"source/fetch_item_failed\"\n | \"source/load_markdown_failed\"\n | \"source/load_blocks_failed\"\n | \"source/blocks_unsupported\"\n | \"cache/io_failed\"\n | \"cache/image_fetch_failed\"\n | \"cache/image_invalid_content_type\"\n | \"renderer/failed\"\n | \"swr/item_check_failed\"\n | \"swr/list_check_failed\"\n | \"swr/content_rebuild_failed\"\n | \"cli/config_invalid\"\n | \"cli/config_load_failed\"\n | \"cli/schema_invalid\"\n | \"cli/generate_failed\"\n | \"cli/init_failed\"\n | \"cli/notion_api_failed\"\n | \"cli/env_file_not_found\";\n\n/**\n * CMS エラーコード。\n * `BuiltInCMSErrorCode` のリテラル補完を維持しつつ、\n * サードパーティアダプタが独自コードを定義できるよう `string & {}` で拡張可能にする。\n */\nexport type CMSErrorCode = BuiltInCMSErrorCode | (string & {});\n\nexport interface CMSErrorContext {\n operation: string;\n slug?: string;\n dataSourceId?: string;\n pageId?: string;\n [key: string]: string | number | boolean | null | undefined;\n}\n\n/**\n * 組み込みエラーコードごとのドキュメント URL アンカーと既定の次アクション。\n * `CMSError` コンストラクタが呼び出し側の `docsUrl` / `nextSteps` 未指定時のフォールバックに使用する。\n *\n * `docs/ja/errors/index.md` の各セクション ID と同期させること。\n */\nconst ERROR_DOCS_BASE =\n \"https://github.com/kjfsm/notion-headless-cms/blob/main/docs/ja/errors/index.md\";\n\ninterface ErrorHelp {\n readonly docsAnchor: string;\n readonly nextSteps: readonly string[];\n}\n\nconst BUILT_IN_ERROR_HELP: Record<BuiltInCMSErrorCode, ErrorHelp> = {\n \"core/config_invalid\": {\n docsAnchor: \"core-config_invalid\",\n nextSteps: [\n \"createClient の必須オプション (sources / collections) を確認する\",\n \"NOTION_TOKEN が環境変数または .dev.vars に設定されているか確認する\",\n ],\n },\n \"core/schema_invalid\": {\n docsAnchor: \"core-schema_invalid\",\n nextSteps: [\n \"nhc generate を再実行して schema を最新化する\",\n \"PropertyMap / PropertyDef の型が定義どおりか確認する\",\n ],\n },\n \"core/notion_orm_missing\": {\n docsAnchor: \"core-notion_orm_missing\",\n nextSteps: [\n \"@notion-headless-cms/notion-orm をインストールする\",\n \"実行ランタイムが動的 import を許可しているか確認する\",\n ],\n },\n \"core/sort_unsupported_type\": {\n docsAnchor: \"core-sort_unsupported_type\",\n nextSteps: [\n \"sort.by に指定したフィールドの値型が string / number になっているか確認する\",\n \"別フィールドでソートするか、source 側で正規化する\",\n ],\n },\n \"webhook/signature_invalid\": {\n docsAnchor: \"webhook-signature_invalid\",\n nextSteps: [\n \"Notion 側で発行された webhook secret が一致しているか確認する\",\n \"プロキシ / WAF がリクエストボディを書き換えていないか確認する\",\n ],\n },\n \"webhook/payload_invalid\": {\n docsAnchor: \"webhook-payload_invalid\",\n nextSteps: [\n \"リクエストボディが JSON で送信されているか確認する\",\n \"DataSource.parseWebhook の期待するフィールド構造と一致しているか確認する\",\n ],\n },\n \"webhook/unknown_collection\": {\n docsAnchor: \"webhook-unknown_collection\",\n nextSteps: [\n \"createClient の sources / collections に該当 collection が登録されているか確認する\",\n \"Webhook URL が `?collection=` で正しいコレクション名を指しているか確認する\",\n ],\n },\n \"webhook/not_implemented\": {\n docsAnchor: \"webhook-not_implemented\",\n nextSteps: [\n \"対象 collection の DataSource に parseWebhook を実装する\",\n \"Webhook を使わない場合は cms.invalidate() を直接呼ぶ運用に切り替える\",\n ],\n },\n \"source/fetch_items_failed\": {\n docsAnchor: \"source-fetch_items_failed\",\n nextSteps: [\n \"NOTION_TOKEN がインテグレーションに紐づいているか確認する\",\n \"Notion DB がインテグレーションに接続済みか確認する\",\n \"ネットワーク / Notion API の障害状況 (status.notion.so) を確認する\",\n \"rateLimiter.maxRetries / baseDelayMs を調整してリトライ余地を増やす\",\n ],\n },\n \"source/fetch_item_failed\": {\n docsAnchor: \"source-fetch_item_failed\",\n nextSteps: [\n \"slug プロパティが Notion DB に存在し、値がユニークか確認する\",\n \"対象ページがインテグレーションに共有されているか確認する\",\n ],\n },\n \"source/load_markdown_failed\": {\n docsAnchor: \"source-load_markdown_failed\",\n nextSteps: [\n \"対象ページがアーカイブされていないか確認する\",\n \"未対応ブロック (file / video など) が原因なら fetch 戦略を `markdownFetcher()` に切り替えるか、対象ブロックを除外する\",\n ],\n },\n \"source/load_blocks_failed\": {\n docsAnchor: \"source-load_blocks_failed\",\n nextSteps: [\n \"対象ページが削除 / アーカイブされていないか確認する\",\n \"Notion API の rate limit に当たっていないかログで確認する\",\n ],\n },\n \"source/blocks_unsupported\": {\n docsAnchor: \"source-blocks_unsupported\",\n nextSteps: [\n \"react-renderer を使う場合は fetch 戦略を `fetchBlockTree()` に切り替える\",\n \"あるいは markdown 経路で本文表示にフォールバックする\",\n ],\n },\n \"cache/io_failed\": {\n docsAnchor: \"cache-io_failed\",\n nextSteps: [\n \"KV / R2 / メモリキャッシュの binding (env.DOC_CACHE / env.IMG_BUCKET) が正しいか確認する\",\n \"wrangler.toml の binding 名と createClient に渡した env が一致しているか確認する\",\n \"一時的な障害なら SWR が次回読み込み時に自己回復する\",\n ],\n },\n \"cache/image_fetch_failed\": {\n docsAnchor: \"cache-image_fetch_failed\",\n nextSteps: [\n \"Notion 署名 URL の有効期限 (約 1 時間) が切れていないか確認する\",\n \"Worker / Node のアウトバウンドネットワークが許可されているか確認する\",\n ],\n },\n \"cache/image_invalid_content_type\": {\n docsAnchor: \"cache-image_invalid_content_type\",\n nextSteps: [\n \"Notion 画像 URL を直接ブラウザで開いて image/* を返すか確認する\",\n \"プロキシ / CDN が Content-Type を書き換えていないか確認する\",\n ],\n },\n \"renderer/failed\": {\n docsAnchor: \"renderer-failed\",\n nextSteps: [\n \"renderer に渡している remark / rehype プラグインの組み合わせを確認する\",\n \"fetch 戦略と renderer の組み合わせが整合しているか (Notion enhanced markdown には notionMarkdownRenderer が必要) を確認する\",\n ],\n },\n \"swr/item_check_failed\": {\n docsAnchor: \"swr-item_check_failed\",\n nextSteps: [\n \"ログで cause を確認し、source/fetch_item_failed と同じ手順で原因を切り分ける\",\n \"バックグラウンドの失敗は次回 SWR で自動再試行されるため恒久対処不要なケースもある\",\n ],\n },\n \"swr/list_check_failed\": {\n docsAnchor: \"swr-list_check_failed\",\n nextSteps: [\n \"ログで cause を確認し、source/fetch_items_failed と同じ手順で原因を切り分ける\",\n \"Notion API の rate limit に近い場合は rateLimiter を絞る\",\n ],\n },\n \"swr/content_rebuild_failed\": {\n docsAnchor: \"swr-content_rebuild_failed\",\n nextSteps: [\n \"renderer / loadMarkdown が一時的に失敗しただけならログを確認のうえ放置可\",\n \"恒常的に再発する場合は対象 slug を fresh: true で取り直して再現を確認する\",\n ],\n },\n \"cli/config_invalid\": {\n docsAnchor: \"cli-config_invalid\",\n nextSteps: [\n \"nhc.config.ts が defineConfig() を default export しているか確認する\",\n \"collections に少なくとも 1 件、databaseId または dbName が指定されているか確認する\",\n ],\n },\n \"cli/config_load_failed\": {\n docsAnchor: \"cli-config_load_failed\",\n nextSteps: [\n \"nhc.config.ts に構文エラーがないか tsc / エディタで確認する\",\n \"ESM の import パスが拡張子付き (.js) になっているか確認する\",\n ],\n },\n \"cli/schema_invalid\": {\n docsAnchor: \"cli-schema_invalid\",\n nextSteps: [\n \"Notion DB のプロパティ型が CLI 対応 (title / richText / select / status / multiSelect / date / number / checkbox / url) のいずれかか確認する\",\n \"未対応プロパティをスキップするか、Notion 側で型を変える\",\n ],\n },\n \"cli/generate_failed\": {\n docsAnchor: \"cli-generate_failed\",\n nextSteps: [\n \"--verbose を付けて再実行し、失敗箇所のスタックトレースを確認する\",\n \"出力先ディレクトリの書き込み権限を確認する\",\n ],\n },\n \"cli/init_failed\": {\n docsAnchor: \"cli-init_failed\",\n nextSteps: [\n \"出力先パスに既存ファイルがある場合は --force を付けるか別パスを指定する\",\n \"親ディレクトリの書き込み権限を確認する\",\n ],\n },\n \"cli/notion_api_failed\": {\n docsAnchor: \"cli-notion_api_failed\",\n nextSteps: [\n \"NOTION_TOKEN がインテグレーションに紐づいているか確認する\",\n \"対象 DB がインテグレーションに接続されているか (Notion DB → … → Connections) 確認する\",\n \"DB 名で解決している場合は完全一致 (前後空白 / 全角半角) を確認する\",\n \"--verbose で Notion API レスポンスの status / code を確認する\",\n ],\n },\n \"cli/env_file_not_found\": {\n docsAnchor: \"cli-env_file_not_found\",\n nextSteps: [\n \"--env-file で指定したパスを実ファイルパスで確認する\",\n \"プロジェクトルートで実行しているか (相対パスは cwd 基準)\",\n ],\n },\n};\n\nfunction lookupBuiltInHelp(code: CMSErrorCode): ErrorHelp | undefined {\n return BUILT_IN_ERROR_HELP[code as BuiltInCMSErrorCode];\n}\n\nexport class CMSError extends Error {\n readonly code: CMSErrorCode;\n override readonly cause?: unknown;\n readonly context: CMSErrorContext;\n /** エラーを解消するための次のアクション(表示用)。 */\n readonly nextSteps?: readonly string[];\n /** 詳細ドキュメントへの URL(表示用)。 */\n readonly docsUrl?: string;\n\n constructor(params: {\n code: CMSErrorCode;\n message: string;\n cause?: unknown;\n context: CMSErrorContext;\n nextSteps?: readonly string[];\n docsUrl?: string;\n }) {\n super(params.message, { cause: params.cause });\n this.name = \"CMSError\";\n this.code = params.code;\n this.cause = params.cause;\n this.context = params.context;\n\n // 呼び出し側が nextSteps / docsUrl を指定していない場合は、組み込みコード向けの既定値で補完する。\n // サードパーティコードや未登録コードは undefined のままにする。\n const help = lookupBuiltInHelp(params.code);\n this.nextSteps = params.nextSteps ?? help?.nextSteps;\n this.docsUrl =\n params.docsUrl ??\n (help ? `${ERROR_DOCS_BASE}#${help.docsAnchor}` : undefined);\n }\n\n /** エラーコードが指定した値と一致するか判定する。 */\n is(code: CMSErrorCode): boolean {\n return this.code === code;\n }\n\n /** エラーコードが指定した名前空間に属するか判定する(例: `\"source/\"`)。 */\n inNamespace(namespace: string): boolean {\n return this.code.startsWith(namespace);\n }\n\n /**\n * nextSteps と docsUrl を含む人間向けの詳細メッセージを返す。\n * エラーダイアログ・ログ出力時に使う。\n */\n format(): string {\n const lines: string[] = [this.message];\n if (this.nextSteps?.length) {\n lines.push(\"\\n次にやること:\");\n for (const step of this.nextSteps) {\n lines.push(` - ${step}`);\n }\n }\n if (this.docsUrl) {\n lines.push(`\\n詳細: ${this.docsUrl}`);\n }\n return lines.join(\"\\n\");\n }\n}\n\nexport function isCMSError(error: unknown): error is CMSError {\n return error instanceof CMSError;\n}\n\n/** エラーコードが特定の名前空間に属するかを判定する(例: \"source/\")。 */\nexport function isCMSErrorInNamespace(\n error: unknown,\n namespace: string,\n): error is CMSError {\n return isCMSError(error) && error.code.startsWith(namespace);\n}\n\ntype CMSErrorHandler<R> = (err: CMSError) => R;\n\n/**\n * `CMSError` を switch 式のように分岐して処理するユーティリティ。\n * `_` キーはフォールバック(CMSError 以外 or 未マッチ時)に使われる。\n *\n * @example\n * matchCMSError(err, {\n * \"source/fetch_items_failed\": (e) => handleFetchError(e),\n * _: (e) => { throw e; },\n * });\n */\nexport function matchCMSError<R>(\n error: unknown,\n handlers: Partial<Record<CMSErrorCode, CMSErrorHandler<R>>> & {\n _?: (err: unknown) => R;\n },\n): R | undefined {\n if (!isCMSError(error)) {\n return handlers._?.(error);\n }\n const handler =\n handlers[error.code as CMSErrorCode] ??\n (handlers._ as CMSErrorHandler<R> | undefined);\n return handler?.(error);\n}\n"],"mappings":";;;;;;;AAqFA,MAAM,kBACJ;AAOF,MAAM,sBAA8D;CAClE,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,uDACA,+CACF;CACF;CACA,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,qCACA,yCACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,6CACA,gCACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,sDACA,8BACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,8CACA,oCACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,gCACA,kDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,qEACA,qDACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,mDACA,iDACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW;GACT;GACA;GACA;GACA;EACF;CACF;CACA,4BAA4B;EAC1B,YAAY;EACZ,WAAW,CACT,0CACA,8BACF;CACF;CACA,+BAA+B;EAC7B,YAAY;EACZ,WAAW,CACT,0BACA,mFACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,+BACA,0CACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,6DACA,iCACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW;GACT;GACA;GACA;EACF;CACF;CACA,4BAA4B;EAC1B,YAAY;EACZ,WAAW,CACT,6CACA,2CACF;CACF;CACA,oCAAoC;EAClC,YAAY;EACZ,WAAW,CACT,8CACA,0CACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW,CACT,oDACA,kGACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW,CACT,0DACA,6CACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW,CACT,2DACA,gDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,oDACA,gDACF;CACF;CACA,sBAAsB;EACpB,YAAY;EACZ,WAAW,CACT,6DACA,4DACF;CACF;CACA,0BAA0B;EACxB,YAAY;EACZ,WAAW,CACT,4CACA,yCACF;CACF;CACA,sBAAsB;EACpB,YAAY;EACZ,WAAW,CACT,4HACA,iCACF;CACF;CACA,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,yCACA,uBACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW,CACT,4CACA,qBACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW;GACT;GACA;GACA;GACA;EACF;CACF;CACA,0BAA0B;EACxB,YAAY;EACZ,WAAW,CACT,mCACA,kCACF;CACF;AACF;AAEA,SAAS,kBAAkB,MAA2C;CACpE,OAAO,oBAAoB;AAC7B;AAEA,IAAa,WAAb,cAA8B,MAAM;CAClC;CACA;CACA;;CAEA;;CAEA;CAEA,YAAY,QAOT;EACD,MAAM,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM,CAAC;EAC7C,KAAK,OAAO;EACZ,KAAK,OAAO,OAAO;EACnB,KAAK,QAAQ,OAAO;EACpB,KAAK,UAAU,OAAO;EAItB,MAAM,OAAO,kBAAkB,OAAO,IAAI;EAC1C,KAAK,YAAY,OAAO,aAAa,MAAM;EAC3C,KAAK,UACH,OAAO,YACN,OAAO,GAAG,gBAAgB,GAAG,KAAK,eAAe,KAAA;CACtD;;CAGA,GAAG,MAA6B;EAC9B,OAAO,KAAK,SAAS;CACvB;;CAGA,YAAY,WAA4B;EACtC,OAAO,KAAK,KAAK,WAAW,SAAS;CACvC;;;;;CAMA,SAAiB;EACf,MAAM,QAAkB,CAAC,KAAK,OAAO;EACrC,IAAI,KAAK,WAAW,QAAQ;GAC1B,MAAM,KAAK,WAAW;GACtB,KAAK,MAAM,QAAQ,KAAK,WACtB,MAAM,KAAK,OAAO,MAAM;EAE5B;EACA,IAAI,KAAK,SACP,MAAM,KAAK,SAAS,KAAK,SAAS;EAEpC,OAAO,MAAM,KAAK,IAAI;CACxB;AACF;AAEA,SAAgB,WAAW,OAAmC;CAC5D,OAAO,iBAAiB;AAC1B;;AAGA,SAAgB,sBACd,OACA,WACmB;CACnB,OAAO,WAAW,KAAK,KAAK,MAAM,KAAK,WAAW,SAAS;AAC7D;;;;;;;;;;;AAcA,SAAgB,cACd,OACA,UAGe;CACf,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,SAAS,IAAI,KAAK;CAK3B,QAFE,SAAS,MAAM,SACd,SAAS,KACK,KAAK;AACxB"}
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * ライブラリ組み込みの CMS エラーコード。\n *\n * | コード | 発生条件 |\n * |---|---|\n * | `core/config_invalid` | 設定不備(token 未設定など) |\n * | `core/schema_invalid` | schema/mapping の型不整合 |\n * | `core/notion_orm_missing` | `@notion-headless-cms/notion-orm` の動的ロード失敗 |\n * | `core/sort_unsupported_type` | ソートキーの値型が string / number でない |\n * | `webhook/signature_invalid` | Webhook 署名検証失敗 |\n * | `webhook/payload_invalid` | Webhook ペイロード形式不正 |\n * | `webhook/unknown_collection` | Webhook の対象コレクションが未知 |\n * | `webhook/not_implemented` | DataSource が parseWebhook を実装していない |\n * | `handler/unknown_collection` | `cms.handler()` のルート (versions / check) が指す対象コレクションが未知 |\n * | `source/fetch_items_failed` | `DataSource.list()` 失敗 |\n * | `source/fetch_item_failed` | `DataSource.findByProp()` 失敗 |\n * | `source/load_markdown_failed` | `DataSource.loadMarkdown()` 失敗 |\n * | `source/load_blocks_failed` | `DataSource.loadBlocks()` 失敗 |\n * | `source/blocks_unsupported` | 選択した fetch 戦略が NotionBlockTree 取得を提供していない (markdown 戦略選択時など) |\n * | `cache/io_failed` | document / image キャッシュの I/O 失敗 |\n * | `cache/image_fetch_failed` | Notion 画像の HTTP 取得失敗 |\n * | `cache/image_invalid_content_type` | 画像レスポンスの Content-Type が不正 |\n * | `renderer/failed` | Markdown → HTML 変換失敗 |\n * | `swr/item_check_failed` | SWR バックグラウンドのアイテム差分チェック失敗 |\n * | `swr/list_check_failed` | SWR バックグラウンドのリスト差分チェック失敗 |\n * | `swr/content_rebuild_failed` | SWR バックグラウンドの本文再生成失敗 |\n * | `cli/config_invalid` | `nhc.config.ts` の内容不整合 |\n * | `cli/config_load_failed` | 設定ファイルの読み込み / 評価失敗 |\n * | `cli/schema_invalid` | CLI が受け取ったスキーマ / マッピング不整合 |\n * | `cli/generate_failed` | `nhc generate` の処理失敗 |\n * | `cli/init_failed` | `nhc init` の処理失敗 |\n * | `cli/notion_api_failed` | CLI が Notion API を呼び出す際の失敗 |\n * | `cli/env_file_not_found` | `--env-file` で指定したファイルが存在しない |\n *\n * サードパーティアダプタが独自コードを追加したい場合は `CMSErrorCode` を参照。\n */\nexport type BuiltInCMSErrorCode =\n | \"core/config_invalid\"\n | \"core/schema_invalid\"\n | \"core/notion_orm_missing\"\n | \"core/sort_unsupported_type\"\n | \"webhook/signature_invalid\"\n | \"webhook/payload_invalid\"\n | \"webhook/unknown_collection\"\n | \"webhook/not_implemented\"\n | \"handler/unknown_collection\"\n | \"source/fetch_items_failed\"\n | \"source/fetch_item_failed\"\n | \"source/load_markdown_failed\"\n | \"source/load_blocks_failed\"\n | \"source/blocks_unsupported\"\n | \"cache/io_failed\"\n | \"cache/image_fetch_failed\"\n | \"cache/image_invalid_content_type\"\n | \"renderer/failed\"\n | \"swr/item_check_failed\"\n | \"swr/list_check_failed\"\n | \"swr/content_rebuild_failed\"\n | \"cli/config_invalid\"\n | \"cli/config_load_failed\"\n | \"cli/schema_invalid\"\n | \"cli/generate_failed\"\n | \"cli/init_failed\"\n | \"cli/notion_api_failed\"\n | \"cli/env_file_not_found\";\n\n/**\n * CMS エラーコード。\n * `BuiltInCMSErrorCode` のリテラル補完を維持しつつ、\n * サードパーティアダプタが独自コードを定義できるよう `string & {}` で拡張可能にする。\n */\nexport type CMSErrorCode = BuiltInCMSErrorCode | (string & {});\n\nexport interface CMSErrorContext {\n operation: string;\n slug?: string;\n dataSourceId?: string;\n pageId?: string;\n [key: string]: string | number | boolean | null | undefined;\n}\n\n/**\n * 組み込みエラーコードごとのドキュメント URL アンカーと既定の次アクション。\n * `CMSError` コンストラクタが呼び出し側の `docsUrl` / `nextSteps` 未指定時のフォールバックに使用する。\n *\n * `docs/ja/errors/index.md` の各セクション ID と同期させること。\n */\nconst ERROR_DOCS_BASE =\n \"https://github.com/kjfsm/notion-headless-cms/blob/main/docs/ja/errors/index.md\";\n\ninterface ErrorHelp {\n readonly docsAnchor: string;\n readonly nextSteps: readonly string[];\n}\n\nconst BUILT_IN_ERROR_HELP: Record<BuiltInCMSErrorCode, ErrorHelp> = {\n \"core/config_invalid\": {\n docsAnchor: \"core-config_invalid\",\n nextSteps: [\n \"createClient の必須オプション (sources / collections) を確認する\",\n \"NOTION_TOKEN が環境変数または .dev.vars に設定されているか確認する\",\n ],\n },\n \"core/schema_invalid\": {\n docsAnchor: \"core-schema_invalid\",\n nextSteps: [\n \"nhc generate を再実行して schema を最新化する\",\n \"PropertyMap / PropertyDef の型が定義どおりか確認する\",\n ],\n },\n \"core/notion_orm_missing\": {\n docsAnchor: \"core-notion_orm_missing\",\n nextSteps: [\n \"@notion-headless-cms/notion-orm をインストールする\",\n \"実行ランタイムが動的 import を許可しているか確認する\",\n ],\n },\n \"core/sort_unsupported_type\": {\n docsAnchor: \"core-sort_unsupported_type\",\n nextSteps: [\n \"sort.by に指定したフィールドの値型が string / number になっているか確認する\",\n \"別フィールドでソートするか、source 側で正規化する\",\n ],\n },\n \"webhook/signature_invalid\": {\n docsAnchor: \"webhook-signature_invalid\",\n nextSteps: [\n \"Notion 側で発行された webhook secret が一致しているか確認する\",\n \"プロキシ / WAF がリクエストボディを書き換えていないか確認する\",\n ],\n },\n \"webhook/payload_invalid\": {\n docsAnchor: \"webhook-payload_invalid\",\n nextSteps: [\n \"リクエストボディが JSON で送信されているか確認する\",\n \"DataSource.parseWebhook の期待するフィールド構造と一致しているか確認する\",\n ],\n },\n \"webhook/unknown_collection\": {\n docsAnchor: \"webhook-unknown_collection\",\n nextSteps: [\n \"createClient の sources / collections に該当 collection が登録されているか確認する\",\n \"Webhook URL が `?collection=` で正しいコレクション名を指しているか確認する\",\n ],\n },\n \"webhook/not_implemented\": {\n docsAnchor: \"webhook-not_implemented\",\n nextSteps: [\n \"対象 collection の DataSource に parseWebhook を実装する\",\n \"Webhook を使わない場合は cms.invalidate() を直接呼ぶ運用に切り替える\",\n ],\n },\n \"handler/unknown_collection\": {\n docsAnchor: \"handler-unknown_collection\",\n nextSteps: [\n \"versions / check のルート URL が登録済みのコレクション名を指しているか確認する\",\n \"createCMS の collections に該当コレクションが定義されているか確認する\",\n ],\n },\n \"source/fetch_items_failed\": {\n docsAnchor: \"source-fetch_items_failed\",\n nextSteps: [\n \"NOTION_TOKEN がインテグレーションに紐づいているか確認する\",\n \"Notion DB がインテグレーションに接続済みか確認する\",\n \"ネットワーク / Notion API の障害状況 (status.notion.so) を確認する\",\n \"rateLimiter.maxRetries / baseDelayMs を調整してリトライ余地を増やす\",\n ],\n },\n \"source/fetch_item_failed\": {\n docsAnchor: \"source-fetch_item_failed\",\n nextSteps: [\n \"slug プロパティが Notion DB に存在し、値がユニークか確認する\",\n \"対象ページがインテグレーションに共有されているか確認する\",\n ],\n },\n \"source/load_markdown_failed\": {\n docsAnchor: \"source-load_markdown_failed\",\n nextSteps: [\n \"対象ページがアーカイブされていないか確認する\",\n \"未対応ブロック (file / video など) が原因なら fetch 戦略を `markdownFetcher()` に切り替えるか、対象ブロックを除外する\",\n ],\n },\n \"source/load_blocks_failed\": {\n docsAnchor: \"source-load_blocks_failed\",\n nextSteps: [\n \"対象ページが削除 / アーカイブされていないか確認する\",\n \"Notion API の rate limit に当たっていないかログで確認する\",\n ],\n },\n \"source/blocks_unsupported\": {\n docsAnchor: \"source-blocks_unsupported\",\n nextSteps: [\n \"react-renderer を使う場合は fetch 戦略を `fetchBlockTree()` に切り替える\",\n \"あるいは markdown 経路で本文表示にフォールバックする\",\n ],\n },\n \"cache/io_failed\": {\n docsAnchor: \"cache-io_failed\",\n nextSteps: [\n \"KV / R2 / メモリキャッシュの binding (env.DOC_CACHE / env.IMG_BUCKET) が正しいか確認する\",\n \"wrangler.toml の binding 名と createClient に渡した env が一致しているか確認する\",\n \"一時的な障害なら SWR が次回読み込み時に自己回復する\",\n ],\n },\n \"cache/image_fetch_failed\": {\n docsAnchor: \"cache-image_fetch_failed\",\n nextSteps: [\n \"Notion 署名 URL の有効期限 (約 1 時間) が切れていないか確認する\",\n \"Worker / Node のアウトバウンドネットワークが許可されているか確認する\",\n ],\n },\n \"cache/image_invalid_content_type\": {\n docsAnchor: \"cache-image_invalid_content_type\",\n nextSteps: [\n \"Notion 画像 URL を直接ブラウザで開いて image/* を返すか確認する\",\n \"プロキシ / CDN が Content-Type を書き換えていないか確認する\",\n ],\n },\n \"renderer/failed\": {\n docsAnchor: \"renderer-failed\",\n nextSteps: [\n \"renderer に渡している remark / rehype プラグインの組み合わせを確認する\",\n \"fetch 戦略と renderer の組み合わせが整合しているか (Notion enhanced markdown には notionMarkdownRenderer が必要) を確認する\",\n ],\n },\n \"swr/item_check_failed\": {\n docsAnchor: \"swr-item_check_failed\",\n nextSteps: [\n \"ログで cause を確認し、source/fetch_item_failed と同じ手順で原因を切り分ける\",\n \"バックグラウンドの失敗は次回 SWR で自動再試行されるため恒久対処不要なケースもある\",\n ],\n },\n \"swr/list_check_failed\": {\n docsAnchor: \"swr-list_check_failed\",\n nextSteps: [\n \"ログで cause を確認し、source/fetch_items_failed と同じ手順で原因を切り分ける\",\n \"Notion API の rate limit に近い場合は rateLimiter を絞る\",\n ],\n },\n \"swr/content_rebuild_failed\": {\n docsAnchor: \"swr-content_rebuild_failed\",\n nextSteps: [\n \"renderer / loadMarkdown が一時的に失敗しただけならログを確認のうえ放置可\",\n \"恒常的に再発する場合は対象 slug を fresh: true で取り直して再現を確認する\",\n ],\n },\n \"cli/config_invalid\": {\n docsAnchor: \"cli-config_invalid\",\n nextSteps: [\n \"nhc.config.ts が defineConfig() を default export しているか確認する\",\n \"collections に少なくとも 1 件、databaseId または dbName が指定されているか確認する\",\n ],\n },\n \"cli/config_load_failed\": {\n docsAnchor: \"cli-config_load_failed\",\n nextSteps: [\n \"nhc.config.ts に構文エラーがないか tsc / エディタで確認する\",\n \"ESM の import パスが拡張子付き (.js) になっているか確認する\",\n ],\n },\n \"cli/schema_invalid\": {\n docsAnchor: \"cli-schema_invalid\",\n nextSteps: [\n \"Notion DB のプロパティ型が CLI 対応 (title / richText / select / status / multiSelect / date / number / checkbox / url) のいずれかか確認する\",\n \"未対応プロパティをスキップするか、Notion 側で型を変える\",\n ],\n },\n \"cli/generate_failed\": {\n docsAnchor: \"cli-generate_failed\",\n nextSteps: [\n \"--verbose を付けて再実行し、失敗箇所のスタックトレースを確認する\",\n \"出力先ディレクトリの書き込み権限を確認する\",\n ],\n },\n \"cli/init_failed\": {\n docsAnchor: \"cli-init_failed\",\n nextSteps: [\n \"出力先パスに既存ファイルがある場合は --force を付けるか別パスを指定する\",\n \"親ディレクトリの書き込み権限を確認する\",\n ],\n },\n \"cli/notion_api_failed\": {\n docsAnchor: \"cli-notion_api_failed\",\n nextSteps: [\n \"NOTION_TOKEN がインテグレーションに紐づいているか確認する\",\n \"対象 DB がインテグレーションに接続されているか (Notion DB → … → Connections) 確認する\",\n \"DB 名で解決している場合は完全一致 (前後空白 / 全角半角) を確認する\",\n \"--verbose で Notion API レスポンスの status / code を確認する\",\n ],\n },\n \"cli/env_file_not_found\": {\n docsAnchor: \"cli-env_file_not_found\",\n nextSteps: [\n \"--env-file で指定したパスを実ファイルパスで確認する\",\n \"プロジェクトルートで実行しているか (相対パスは cwd 基準)\",\n ],\n },\n};\n\nfunction lookupBuiltInHelp(code: CMSErrorCode): ErrorHelp | undefined {\n return BUILT_IN_ERROR_HELP[code as BuiltInCMSErrorCode];\n}\n\nexport class CMSError extends Error {\n readonly code: CMSErrorCode;\n override readonly cause?: unknown;\n readonly context: CMSErrorContext;\n /** エラーを解消するための次のアクション(表示用)。 */\n readonly nextSteps?: readonly string[];\n /** 詳細ドキュメントへの URL(表示用)。 */\n readonly docsUrl?: string;\n\n constructor(params: {\n code: CMSErrorCode;\n message: string;\n cause?: unknown;\n context: CMSErrorContext;\n nextSteps?: readonly string[];\n docsUrl?: string;\n }) {\n super(params.message, { cause: params.cause });\n this.name = \"CMSError\";\n this.code = params.code;\n this.cause = params.cause;\n this.context = params.context;\n\n // 呼び出し側が nextSteps / docsUrl を指定していない場合は、組み込みコード向けの既定値で補完する。\n // サードパーティコードや未登録コードは undefined のままにする。\n const help = lookupBuiltInHelp(params.code);\n this.nextSteps = params.nextSteps ?? help?.nextSteps;\n this.docsUrl =\n params.docsUrl ??\n (help ? `${ERROR_DOCS_BASE}#${help.docsAnchor}` : undefined);\n }\n\n /** エラーコードが指定した値と一致するか判定する。 */\n is(code: CMSErrorCode): boolean {\n return this.code === code;\n }\n\n /** エラーコードが指定した名前空間に属するか判定する(例: `\"source/\"`)。 */\n inNamespace(namespace: string): boolean {\n return this.code.startsWith(namespace);\n }\n\n /**\n * nextSteps と docsUrl を含む人間向けの詳細メッセージを返す。\n * エラーダイアログ・ログ出力時に使う。\n */\n format(): string {\n const lines: string[] = [this.message];\n if (this.nextSteps?.length) {\n lines.push(\"\\n次にやること:\");\n for (const step of this.nextSteps) {\n lines.push(` - ${step}`);\n }\n }\n if (this.docsUrl) {\n lines.push(`\\n詳細: ${this.docsUrl}`);\n }\n return lines.join(\"\\n\");\n }\n}\n\nexport function isCMSError(error: unknown): error is CMSError {\n return error instanceof CMSError;\n}\n\n/** エラーコードが特定の名前空間に属するかを判定する(例: \"source/\")。 */\nexport function isCMSErrorInNamespace(\n error: unknown,\n namespace: string,\n): error is CMSError {\n return isCMSError(error) && error.code.startsWith(namespace);\n}\n\ntype CMSErrorHandler<R> = (err: CMSError) => R;\n\n/**\n * `CMSError` を switch 式のように分岐して処理するユーティリティ。\n * `_` キーはフォールバック(CMSError 以外 or 未マッチ時)に使われる。\n *\n * @example\n * matchCMSError(err, {\n * \"source/fetch_items_failed\": (e) => handleFetchError(e),\n * _: (e) => { throw e; },\n * });\n */\nexport function matchCMSError<R>(\n error: unknown,\n handlers: Partial<Record<CMSErrorCode, CMSErrorHandler<R>>> & {\n _?: (err: unknown) => R;\n },\n): R | undefined {\n if (!isCMSError(error)) {\n return handlers._?.(error);\n }\n const handler =\n handlers[error.code as CMSErrorCode] ??\n (handlers._ as CMSErrorHandler<R> | undefined);\n return handler?.(error);\n}\n"],"mappings":";;;;;;;AAuFA,MAAM,kBACJ;AAOF,MAAM,sBAA8D;CAClE,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,uDACA,+CACF;CACF;CACA,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,qCACA,yCACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,6CACA,gCACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,sDACA,8BACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,8CACA,oCACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,gCACA,kDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,qEACA,qDACF;CACF;CACA,2BAA2B;EACzB,YAAY;EACZ,WAAW,CACT,mDACA,iDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,sDACA,gDACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW;GACT;GACA;GACA;GACA;EACF;CACF;CACA,4BAA4B;EAC1B,YAAY;EACZ,WAAW,CACT,0CACA,8BACF;CACF;CACA,+BAA+B;EAC7B,YAAY;EACZ,WAAW,CACT,0BACA,mFACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,+BACA,0CACF;CACF;CACA,6BAA6B;EAC3B,YAAY;EACZ,WAAW,CACT,6DACA,iCACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW;GACT;GACA;GACA;EACF;CACF;CACA,4BAA4B;EAC1B,YAAY;EACZ,WAAW,CACT,6CACA,2CACF;CACF;CACA,oCAAoC;EAClC,YAAY;EACZ,WAAW,CACT,8CACA,0CACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW,CACT,oDACA,kGACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW,CACT,0DACA,6CACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW,CACT,2DACA,gDACF;CACF;CACA,8BAA8B;EAC5B,YAAY;EACZ,WAAW,CACT,oDACA,gDACF;CACF;CACA,sBAAsB;EACpB,YAAY;EACZ,WAAW,CACT,6DACA,4DACF;CACF;CACA,0BAA0B;EACxB,YAAY;EACZ,WAAW,CACT,4CACA,yCACF;CACF;CACA,sBAAsB;EACpB,YAAY;EACZ,WAAW,CACT,4HACA,iCACF;CACF;CACA,uBAAuB;EACrB,YAAY;EACZ,WAAW,CACT,yCACA,uBACF;CACF;CACA,mBAAmB;EACjB,YAAY;EACZ,WAAW,CACT,4CACA,qBACF;CACF;CACA,yBAAyB;EACvB,YAAY;EACZ,WAAW;GACT;GACA;GACA;GACA;EACF;CACF;CACA,0BAA0B;EACxB,YAAY;EACZ,WAAW,CACT,mCACA,kCACF;CACF;AACF;AAEA,SAAS,kBAAkB,MAA2C;CACpE,OAAO,oBAAoB;AAC7B;AAEA,IAAa,WAAb,cAA8B,MAAM;CAClC;CACA;CACA;;CAEA;;CAEA;CAEA,YAAY,QAOT;EACD,MAAM,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM,CAAC;EAC7C,KAAK,OAAO;EACZ,KAAK,OAAO,OAAO;EACnB,KAAK,QAAQ,OAAO;EACpB,KAAK,UAAU,OAAO;EAItB,MAAM,OAAO,kBAAkB,OAAO,IAAI;EAC1C,KAAK,YAAY,OAAO,aAAa,MAAM;EAC3C,KAAK,UACH,OAAO,YACN,OAAO,GAAG,gBAAgB,GAAG,KAAK,eAAe,KAAA;CACtD;;CAGA,GAAG,MAA6B;EAC9B,OAAO,KAAK,SAAS;CACvB;;CAGA,YAAY,WAA4B;EACtC,OAAO,KAAK,KAAK,WAAW,SAAS;CACvC;;;;;CAMA,SAAiB;EACf,MAAM,QAAkB,CAAC,KAAK,OAAO;EACrC,IAAI,KAAK,WAAW,QAAQ;GAC1B,MAAM,KAAK,WAAW;GACtB,KAAK,MAAM,QAAQ,KAAK,WACtB,MAAM,KAAK,OAAO,MAAM;EAE5B;EACA,IAAI,KAAK,SACP,MAAM,KAAK,SAAS,KAAK,SAAS;EAEpC,OAAO,MAAM,KAAK,IAAI;CACxB;AACF;AAEA,SAAgB,WAAW,OAAmC;CAC5D,OAAO,iBAAiB;AAC1B;;AAGA,SAAgB,sBACd,OACA,WACmB;CACnB,OAAO,WAAW,KAAK,KAAK,MAAM,KAAK,WAAW,SAAS;AAC7D;;;;;;;;;;;AAcA,SAAgB,cACd,OACA,UAGe;CACf,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,SAAS,IAAI,KAAK;CAK3B,QAFE,SAAS,MAAM,SACd,SAAS,KACK,KAAK;AACxB"}
package/dist/hooks.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as BaseContentItem } from "./content-DwsfWZao.mjs";
2
- import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BmrOz8T6.mjs";
2
+ import { a as CMSHooks, i as Logger, t as CMSPlugin } from "./plugin-BPhaVO6-.mjs";
3
3
 
4
4
  //#region src/hooks.d.ts
5
5
  /**
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { a as CachedItemMeta, c as ContentResult, i as CachedItemList, l as ImageRef, n as CMSSchemaProperties, o as StorageBinary, r as CachedItemContent, s as ContentBlock, t as BaseContentItem, u as InlineNode } from "./content-DwsfWZao.mjs";
2
- import { a as ImageCacheOps, c as InvalidateScope, d as WebhookConfig, i as DocumentCacheOps, l as PropertyDef, n as CacheAdapterStats, o as DataSource, r as CacheAreaStats, s as InvalidateKind, t as CacheAdapter, u as PropertyMap } from "./cache-v9jTMnYd.mjs";
3
- import { a as isCMSError, i as CMSErrorContext, n as CMSError, o as isCMSErrorInNamespace, r as CMSErrorCode, s as matchCMSError, t as BuiltInCMSErrorCode } from "./errors-DcNErfYk.mjs";
4
- import { a as CMSHooks, i as Logger, n as definePlugin, o as MaybePromise, r as LogContext, t as CMSPlugin } from "./plugin-BmrOz8T6.mjs";
5
- import { _ as MergeSourceCollections, a as DEFAULT_RATE_LIMITER, c as RateLimiterConfig, d as RendererPluginList, f as SWRConfig, g as CMSSources, i as CreateClientOptions, l as RenderOptions, m as defineCollection, n as CollectionsConfig, o as InferCollectionItem, p as StrictCollectionDef, r as ContentConfig, s as LogLevel, u as RendererFn } from "./config-i99tKRhN.mjs";
2
+ import { a as ImageCacheOps, c as InvalidateScope, d as WebhookConfig, i as DocumentCacheOps, l as PropertyDef, n as CacheAdapterStats, o as DataSource, r as CacheAreaStats, s as InvalidateKind, t as CacheAdapter, u as PropertyMap } from "./cache-BnC6kxoU.mjs";
3
+ import { a as isCMSError, i as CMSErrorContext, n as CMSError, o as isCMSErrorInNamespace, r as CMSErrorCode, s as matchCMSError, t as BuiltInCMSErrorCode } from "./errors-DPD5yq9S.mjs";
4
+ import { a as CMSHooks, i as Logger, n as definePlugin, o as MaybePromise, r as LogContext, t as CMSPlugin } from "./plugin-BPhaVO6-.mjs";
5
+ import { _ as MergeSourceCollections, a as DEFAULT_RATE_LIMITER, c as RateLimiterConfig, d as RendererPluginList, f as SWRConfig, g as CMSSources, i as CreateClientOptions, l as RenderOptions, m as defineCollection, n as CollectionsConfig, o as InferCollectionItem, p as StrictCollectionDef, r as ContentConfig, s as LogLevel, u as RendererFn } from "./config-RnU3jrFb.mjs";
6
6
  import { MemoryCacheOptions, memoryCache } from "./cache/memory.mjs";
7
7
  import { mergeHooks, mergeLoggers } from "./hooks.mjs";
8
8
  import { NodePresetOptions, nodePreset } from "./preset/node.mjs";
@@ -96,6 +96,11 @@ interface CollectionCacheOps<T extends BaseContentItem> {
96
96
  * SSG ビルド前のウォームアップに利用する。
97
97
  */
98
98
  warm(opts?: WarmOptions): Promise<WarmResult>;
99
+ /**
100
+ * 指定 slug の1件だけを Notion から再取得し、メタ・本文キャッシュを作り直す(単件ウォーム)。
101
+ * webhook 受信時など、更新された1ページだけを温め直すのに使う。アイテムが存在しなければ何もしない。
102
+ */
103
+ prime(slug: string): Promise<void>;
99
104
  }
100
105
  /** `check()` の戻り値。差分なしか、差分ありの場合はアイテムを含む。 */
101
106
  type CheckResult<T extends BaseContentItem> = {
@@ -167,7 +172,6 @@ declare const noopDocOps: DocumentCacheOps;
167
172
  declare const noopImgOps: ImageCacheOps;
168
173
  //#endregion
169
174
  //#region src/handler.d.ts
170
- /** `$handler()` の挙動設定。 */
171
175
  interface HandlerOptions {
172
176
  /** マウントするベースパス。デフォルト `/api/cms`。 */
173
177
  basePath?: string;
@@ -175,34 +179,81 @@ interface HandlerOptions {
175
179
  imagesPath?: string;
176
180
  /** revalidate webhook のパス (basePath 相対)。デフォルト `/revalidate`。 */
177
181
  revalidatePath?: string;
182
+ /** バージョン照会 (peekVersion) のパス (basePath 相対)。デフォルト `/versions`。 */
183
+ versionsPath?: string;
184
+ /** 更新チェック (check) のパス (basePath 相対)。デフォルト `/check`。 */
185
+ checkPath?: string;
186
+ /** Notion 公式 webhook 受信のパス (basePath 相対)。デフォルト `/notion-webhook`。 */
187
+ notionWebhookPath?: string;
178
188
  /** Webhook 署名検証用シークレット (未指定なら検証スキップ)。 */
179
189
  webhookSecret?: string;
190
+ /** Notion 公式 webhook(integration の Webhooks)の受信設定。 */
191
+ notionWebhook?: {
192
+ /**
193
+ * 検証トークン(HMAC-SHA256 署名キー)。未指定時は createCMS の
194
+ * `notion.webhookSecret`(= `CreateClientOptions.notionWebhookSecret`)を既定で使う。
195
+ */
196
+ secret?: string;
197
+ /**
198
+ * サブスク登録時に Notion が送る `verification_token` を受け取るコールバック。
199
+ * 値を控えて `notion.webhookSecret` に設定する用途(既定ではレスポンス本文にも echo する)。
200
+ */
201
+ onVerificationToken?: (token: string) => void;
202
+ };
180
203
  /** デフォルト実装を無効化する場合 true。 */
181
204
  disabled?: boolean;
182
205
  }
183
- /** `$handler()` が内部で依存する CMS 機能の最小セット。 */
184
206
  interface HandlerAdapter {
185
207
  imageCache: ImageCacheOps;
186
208
  /**
187
209
  * 指定コレクションの DataSource.parseWebhook を呼ぶ。
188
- * 未知コレクション → `webhook/unknown_collection` CMSError
210
+ * 未知コレクション → `handler/unknown_collection` CMSError
189
211
  * parseWebhook 未実装 → `webhook/not_implemented` CMSError
190
212
  */
191
213
  parseWebhookFor(collection: string, req: Request, webhookSecret: string | undefined): Promise<InvalidateScope>;
192
214
  revalidate(scope: InvalidateScope): Promise<void>;
215
+ /**
216
+ * 指定コレクション / slug の `peekVersion`(KV メタのみ、Notion API 非呼び出し)を返す。
217
+ * キャッシュ未登録なら `null`。未知コレクション → `handler/unknown_collection` CMSError。
218
+ */
219
+ peekVersionFor(collection: string, slug: string): Promise<{
220
+ notionUpdatedAt: string;
221
+ cachedAt: number;
222
+ } | null>;
223
+ /**
224
+ * 指定コレクション / slug を `currentVersion` と比較し(Notion を実照会)、
225
+ * 差分があればキャッシュを更新して `stale` を返す。
226
+ * アイテムが存在しない場合は `null`。未知コレクション → `handler/unknown_collection` CMSError。
227
+ */
228
+ checkFor(collection: string, slug: string, currentVersion: string): Promise<{
229
+ stale: boolean;
230
+ } | null>;
231
+ /**
232
+ * Notion ページ ID を全コレクション横断で解決し単件ウォームする(公式 webhook 用)。
233
+ * 一致したコレクション / slug、無ければ `null`。
234
+ */
235
+ warmByPageId(pageId: string): Promise<{
236
+ collection: string;
237
+ slug: string;
238
+ } | null>;
239
+ /** createCMS で設定された Notion webhook 検証トークンの既定値。 */
240
+ notionWebhookSecret?: string;
241
+ /** 応答送信後もウォームを完走させる実行フック (Cloudflare の `waitUntil` 相当)。 */
242
+ scheduleBackground?: (p: Promise<unknown>) => void;
193
243
  }
194
244
  /**
195
245
  * Web Standard な Request → Response ルーター。
196
246
  * Next.js / React Router / Hono / Cloudflare Workers いずれでも使える。
197
247
  *
198
248
  * ルート:
199
- * - GET `{basePath}/images/:hash` — 画像プロキシ
200
- * - POST `{basePath}/revalidate/:collection` Webhook 受信 + $revalidate()
249
+ * - GET `{basePath}/images/:hash` — 画像プロキシ
250
+ * - GET `{basePath}/versions/:collection/:slug` peekVersion(更新検知ポーリング)
251
+ * - GET/POST `{basePath}/check/:collection/:slug?v=` — check(更新を実照会してキャッシュ更新)
252
+ * - POST `{basePath}/revalidate/:collection` — Webhook 受信 + $revalidate()
201
253
  */
202
254
  declare function createHandler(adapter: HandlerAdapter, opts?: HandlerOptions): (req: Request) => Promise<Response>;
203
255
  //#endregion
204
256
  //#region src/cms.d.ts
205
- /** コレクション別アクセス + グローバル操作の合成型。 */
206
257
  type CMSClient<C extends CollectionsConfig> = { [K in keyof C]: CollectionClient<InferCollectionItem<C[K]>> } & CMSGlobalOps;
207
258
  /**
208
259
  * `cms.stats()` が返す集約済みキャッシュ統計。
@@ -235,6 +286,15 @@ interface CMSGlobalOps {
235
286
  /** クライアント単位の trace ID (`createClient` で発行)。 */
236
287
  readonly traceId: string;
237
288
  invalidate(scope?: InvalidateScope): Promise<void>;
289
+ /**
290
+ * Notion ページ ID を全コレクション横断で解決し、該当アイテムを単件ウォームする。
291
+ * 公式 webhook の sparse payload(page id のみ)からミラーを再生成するために使う。
292
+ * 一致したコレクションと slug を返す。どのコレクションにも属さなければ `null`。
293
+ */
294
+ warmByPageId(pageId: string): Promise<{
295
+ collection: string;
296
+ slug: string;
297
+ } | null>;
238
298
  /** Web Standard な Request/Response ベースのルートハンドラ (画像プロキシ + webhook)。 */
239
299
  handler(opts?: HandlerOptions): (req: Request) => Promise<Response>;
240
300
  getCachedImage(hash: string): Promise<StorageBinary | null>;