@notion-headless-cms/core 0.0.6 → 0.0.7

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +174 -115
  2. package/dist/index.js +307 -271
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -3,6 +3,14 @@ import { BlockHandler } from '@notion-headless-cms/transformer';
3
3
  import { PageObjectResponse, RichTextItemResponse } from '@notionhq/client/build/src/api-endpoints';
4
4
  import { PluggableList } from 'unified';
5
5
 
6
+ /** 文字列をSHA-256でハッシュ化し、16進数文字列として返す。画像キーの生成に使用。 */
7
+ declare function sha256Hex(input: string): Promise<string>;
8
+ /**
9
+ * キャッシュが有効期限切れかどうかを判定する。
10
+ * ttlMs が未指定の場合は常に false(無期限有効)を返す。
11
+ */
12
+ declare function isStale(cachedAt: number, ttlMs?: number): boolean;
13
+
6
14
  /**
7
15
  * ライブラリが動作するために必須なフィールド。
8
16
  * 利用者はこのインターフェースを拡張して独自のコンテンツ型を定義する。
@@ -12,7 +20,7 @@ import { PluggableList } from 'unified';
12
20
  * title: string;
13
21
  * author: string;
14
22
  * }
15
- * createCMS<Post>({ schema: { mapItem: (page) => ... } })
23
+ * createCMS<Post>({ source: notionAdapter({ ... }) })
16
24
  */
17
25
  interface BaseContentItem {
18
26
  id: string;
@@ -38,20 +46,6 @@ interface StorageBinary {
38
46
  data: ArrayBuffer;
39
47
  contentType?: string;
40
48
  }
41
- /** CMSコアが依存するストレージ抽象。 */
42
- interface StorageAdapter {
43
- get(key: string): Promise<ArrayBuffer | null>;
44
- put(key: string, data: ArrayBuffer | ArrayBufferView | string, options?: {
45
- contentType?: string;
46
- }): Promise<void>;
47
- json<T>(key: string): Promise<T | null>;
48
- binary(key: string): Promise<StorageBinary | null>;
49
- }
50
- /** ライブラリが必要とする最小限の環境バインディング。 */
51
- interface CMSEnv {
52
- NOTION_TOKEN: string;
53
- NOTION_DATA_SOURCE_ID: string;
54
- }
55
49
  /** Notionのプロパティ名マッピング(すべてオプション)。 */
56
50
  interface CMSSchemaProperties {
57
51
  /** Notionのスラッグプロパティ名。デフォルト: 'Slug' */
@@ -61,127 +55,188 @@ interface CMSSchemaProperties {
61
55
  /** Notionの公開日プロパティ名。デフォルト: 'CreatedAt' */
62
56
  date?: string;
63
57
  }
58
+
59
+ /** ドキュメントキャッシュを抽象化するインターフェース。 */
60
+ interface DocumentCacheAdapter<T extends BaseContentItem = BaseContentItem> {
61
+ readonly name: string;
62
+ getList(): Promise<CachedItemList<T> | null>;
63
+ setList(data: CachedItemList<T>): Promise<void>;
64
+ getItem(slug: string): Promise<CachedItem<T> | null>;
65
+ setItem(slug: string, data: CachedItem<T>): Promise<void>;
66
+ invalidate?(scope: "all" | {
67
+ slug: string;
68
+ } | {
69
+ tag: string;
70
+ }): Promise<void>;
71
+ }
72
+ /** 画像キャッシュを抽象化するインターフェース。 */
73
+ interface ImageCacheAdapter {
74
+ readonly name: string;
75
+ get(hash: string): Promise<StorageBinary | null>;
76
+ set(hash: string, data: ArrayBuffer, contentType: string): Promise<void>;
77
+ }
78
+ /** キャッシュ設定オブジェクト。document / image それぞれ独立したアダプタを差し込める。 */
79
+ interface CacheConfig<T extends BaseContentItem = BaseContentItem> {
80
+ document?: DocumentCacheAdapter<T> | false;
81
+ image?: ImageCacheAdapter | false;
82
+ /** キャッシュの有効期間(ミリ秒)。未設定の場合はTTLなし。 */
83
+ ttlMs?: number;
84
+ }
85
+
64
86
  /**
65
- * CMSの設定オブジェクト。
66
- * ジェネリクス型 T にカスタムコンテンツ型を指定できる(デフォルト: BaseContentItem)。
87
+ * コンテンツソース(Notion など)を抽象化するインターフェース。
88
+ * core Notion の知識を持たず、DataSourceAdapter 経由でのみデータを取得する。
67
89
  */
68
- interface CMSConfig<T extends BaseContentItem = BaseContentItem> {
69
- /** Notion API 認証情報。コンストラクタでクライアントを事前生成するために使用。 */
70
- env?: CMSEnv;
71
- /** キャッシュ/画像保存用ストレージ。未設定時はキャッシュ機能を無効化。 */
72
- storage?: StorageAdapter;
73
- schema?: {
74
- /**
75
- * Notionページをコンテンツ型 T にマッピングするカスタム関数。
76
- * 指定した場合 properties の設定は無視される(slug プロパティ名のみ例外)。
77
- */
78
- mapItem?: (page: PageObjectResponse) => T;
79
- /** mapItem 未使用時のプロパティ名マッピング。 */
80
- properties?: CMSSchemaProperties;
81
- /** getItems() で返す「公開済み」ステータス値の配列。デフォルト: [] (全件返す) */
82
- publishedStatuses?: string[];
83
- /** getItemBySlug() でアクセス可能なステータス値の配列。デフォルト: [] (全件許可) */
84
- accessibleStatuses?: string[];
85
- };
86
- transformer?: {
87
- /** カスタムブロックハンドラーのマップ。Notionブロックタイプをキーとする。 */
88
- blocks?: Record<string, BlockHandler>;
89
- };
90
- renderer?: {
91
- /** 画像プロキシのベースURL。デフォルト: '/api/images' */
92
- imageProxyBase?: string;
93
- /** 追加する remark プラグイン。 */
94
- remarkPlugins?: PluggableList;
95
- /** 追加する rehype プラグイン。 */
96
- rehypePlugins?: PluggableList;
97
- /** デフォルトのパイプラインを置き換えるカスタムレンダラー。 */
98
- render?: RendererFn;
99
- };
100
- cache?: {
101
- /** コンテンツ一覧のキャッシュキー。デフォルト: 'content.json' */
102
- listKey?: string;
103
- /** 個別コンテンツのキャッシュキープレフィックス。デフォルト: 'content/' */
104
- itemPrefix?: string;
105
- /** 画像キャッシュのキープレフィックス。デフォルト: 'images/' */
106
- imagePrefix?: string;
107
- /** キャッシュの有効期間(ミリ秒)。未設定の場合はTTLなし。 */
108
- ttlMs?: number;
109
- };
90
+ interface DataSourceAdapter<T extends BaseContentItem = BaseContentItem> {
91
+ readonly name: string;
92
+ readonly publishedStatuses?: readonly string[];
93
+ readonly accessibleStatuses?: readonly string[];
94
+ list(opts?: {
95
+ publishedStatuses?: readonly string[];
96
+ }): Promise<T[]>;
97
+ findBySlug(slug: string): Promise<T | null>;
98
+ loadMarkdown(item: T): Promise<string>;
110
99
  }
111
100
 
112
- /** 文字列をSHA-256でハッシュ化し、16進数文字列として返す。画像キーの生成に使用。 */
113
- declare function sha256Hex(input: string): Promise<string>;
101
+ /** スキーマ設定。公開ステータスのフィルタやプロパティ名マッピングを制御する。 */
102
+ interface SchemaConfig<T extends BaseContentItem = BaseContentItem> {
103
+ /**
104
+ * Notionページをコンテンツ型 T にマッピングするカスタム関数。
105
+ * 指定した場合 properties の設定は無視される(slug プロパティ名のみ例外)。
106
+ */
107
+ mapItem?: (page: PageObjectResponse) => T;
108
+ /** mapItem 未使用時のプロパティ名マッピング。 */
109
+ properties?: CMSSchemaProperties;
110
+ /** list() で返す「公開済み」ステータス値の配列。デフォルト: [] (全件返す) */
111
+ publishedStatuses?: string[];
112
+ /** findBySlug() でアクセス可能なステータス値の配列。デフォルト: [] (全件許可) */
113
+ accessibleStatuses?: string[];
114
+ }
115
+ /** レンダリング・コンテンツ処理設定。 */
116
+ interface ContentConfig {
117
+ /** 画像プロキシのベースURL。デフォルト: '/api/images' */
118
+ imageProxyBase?: string;
119
+ /** 追加する remark プラグイン。 */
120
+ remarkPlugins?: PluggableList;
121
+ /** 追加する rehype プラグイン。 */
122
+ rehypePlugins?: PluggableList;
123
+ /** デフォルトのパイプラインを置き換えるカスタムレンダラー。 */
124
+ render?: RendererFn;
125
+ /** カスタムブロックハンドラーのマップ。Notionブロックタイプをキーとする。 */
126
+ blocks?: Record<string, BlockHandler>;
127
+ }
114
128
  /**
115
- * キャッシュが有効期限切れかどうかを判定する。
116
- * ttlMs が未指定の場合は常に false(無期限有効)を返す。
129
+ * createCMS() に渡すオプション。
130
+ * ジェネリクス型 T にカスタムコンテンツ型を指定できる(デフォルト: BaseContentItem)。
117
131
  */
118
- declare function isStale(cachedAt: number, ttlMs?: number): boolean;
119
- /** ストレージキャッシュ操作をまとめたヘルパークラス。キープレフィックスは設定で変更可能。 */
120
- declare class CacheStore<T extends BaseContentItem = BaseContentItem> {
121
- private readonly storage?;
122
- private readonly listKey;
123
- private readonly itemPrefix;
124
- private readonly imagePrefix;
125
- constructor(storage: StorageAdapter | undefined, listKey: string, itemPrefix: string, imagePrefix: string);
126
- getItemList(): Promise<CachedItemList<T> | null>;
127
- setItemList(items: T[]): Promise<void>;
128
- getItem(slug: string): Promise<CachedItem<T> | null>;
129
- setItem(slug: string, data: CachedItem<T>): Promise<void>;
130
- getImage(hash: string): Promise<StorageBinary | null>;
131
- setImage(hash: string, data: ArrayBuffer, contentType: string): Promise<void>;
132
+ interface CreateCMSOptions<T extends BaseContentItem = BaseContentItem> {
133
+ /** データソースアダプタ(Notion など)。 */
134
+ source: DataSourceAdapter<T>;
135
+ /** キャッシュ設定。未設定時はキャッシュなし。 */
136
+ cache?: CacheConfig<T>;
137
+ /** スキーマ・ステータス設定。 */
138
+ schema?: SchemaConfig<T>;
139
+ /** レンダリング・コンテンツ処理設定。 */
140
+ content?: ContentConfig;
141
+ /** Cloudflare Workers の waitUntil に相当する非同期処理の登録関数。 */
142
+ waitUntil?: (p: Promise<unknown>) => void;
132
143
  }
133
144
 
145
+ /** インメモリキャッシュ(ドキュメント用)を生成する。 */
146
+ declare function memoryDocumentCache<T extends BaseContentItem = BaseContentItem>(): DocumentCacheAdapter<T>;
147
+ /** インメモリキャッシュ(画像用)を生成する。 */
148
+ declare function memoryImageCache(): ImageCacheAdapter;
134
149
  /**
135
- * Notion をバックエンドとして使う汎用ヘッドレス CMSクラス。
150
+ * ドキュメントと画像の両方にインメモリキャッシュを返す便利関数。
151
+ * memoryCache() はドキュメントキャッシュを返す(後方互換)。
152
+ */
153
+ declare function memoryCache<T extends BaseContentItem = BaseContentItem>(): DocumentCacheAdapter<T>;
154
+
155
+ /** 何もしないドキュメントキャッシュを返す(シングルトン)。 */
156
+ declare function noopDocumentCache<T extends BaseContentItem = BaseContentItem>(): DocumentCacheAdapter<T>;
157
+ /** 何もしない画像キャッシュを返す(シングルトン)。 */
158
+ declare function noopImageCache(): ImageCacheAdapter;
159
+
160
+ /**
161
+ * Notion をバックエンドとして使う汎用ヘッドレス CMS クラス。
136
162
  *
137
163
  * @example
138
164
  * const cms = createCMS({
139
- * env: { NOTION_TOKEN: '...', NOTION_DATA_SOURCE_ID: '...' },
140
- * schema: { publishedStatuses: ['Published'] }
165
+ * source: notionAdapter({ token: '...', dataSourceId: '...' }),
166
+ * schema: { publishedStatuses: ['公開'] },
141
167
  * });
142
- * const items = await cms.getItems();
168
+ * const items = await cms.list();
143
169
  */
144
170
  declare class CMS<T extends BaseContentItem = BaseContentItem> {
145
- private readonly itemMapper;
146
- private readonly slugPropName;
171
+ private readonly source;
172
+ private readonly docCache;
173
+ private readonly imgCache;
174
+ private readonly hasImageCache;
175
+ private readonly ttlMs;
147
176
  private readonly publishedStatuses;
148
177
  private readonly accessibleStatuses;
149
178
  private readonly imageProxyBase;
150
- private readonly transformerConfig;
151
- private readonly rendererConfig;
152
- private readonly store;
153
- private readonly hasStorage;
154
- private readonly ttlMs;
155
- private readonly client;
156
- private readonly dataSourceId;
157
- constructor(config?: CMSConfig<T>);
158
- private requireClient;
159
- /** 公開済みコンテンツ一覧を Notion から直接取得する。 */
160
- getItems(): Promise<T[]>;
161
- /** スラッグでコンテンツを Notion から直接取得する。 */
162
- getItemBySlug(slug: string): Promise<T | null>;
179
+ private readonly contentConfig;
180
+ private readonly waitUntil;
181
+ constructor(opts: CreateCMSOptions<T>);
182
+ /** 公開済みコンテンツ一覧をソースから直接取得する。 */
183
+ list(): Promise<T[]>;
184
+ /** スラッグでコンテンツをソースから直接取得する。 */
185
+ findBySlug(slug: string): Promise<T | null>;
163
186
  /** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
164
187
  isPublished(item: T): boolean;
165
- /** コンテンツをMarkdown→HTMLにレンダリングし、CachedItemとして返す。 */
166
- renderItem(item: T): Promise<CachedItem<T>>;
167
- /** スラッグでコンテンツを取得してMarkdown→HTMLにレンダリングする。 */
168
- renderItemBySlug(slug: string): Promise<CachedItem<T> | null>;
169
- getCachedItemList(): Promise<CachedItemList<T> | null>;
170
- setCachedItemList(items: T[]): Promise<void>;
171
- getCachedItem(slug: string): Promise<CachedItem<T> | null>;
172
- setCachedItem(slug: string, data: CachedItem<T>): Promise<void>;
173
- getCachedImage(hash: string): Promise<StorageBinary | null>;
174
- createCachedImageResponse(hash: string): Promise<Response | null>;
175
- getItemsCachedFirst(options?: {
176
- waitUntil?: (promise: Promise<void>) => void;
188
+ /** コンテンツを Markdown HTML にレンダリングし、CachedItem として返す。 */
189
+ render(item: T): Promise<CachedItem<T>>;
190
+ /** スラッグでコンテンツを取得して Markdown HTML にレンダリングする。 */
191
+ renderBySlug(slug: string): Promise<CachedItem<T> | null>;
192
+ /** ステータスでフィルタリングした一覧を返す。 */
193
+ listByStatus(status: string | readonly string[]): Promise<T[]>;
194
+ /** 任意の条件でフィルタリングした一覧を返す。 */
195
+ where(predicate: (item: T) => boolean): Promise<T[]>;
196
+ /** ページネーション付き一覧を返す。 */
197
+ paginate(opts: {
198
+ page: number;
199
+ perPage: number;
200
+ }): Promise<{
201
+ items: T[];
202
+ total: number;
203
+ page: number;
204
+ perPage: number;
205
+ hasNext: boolean;
206
+ }>;
207
+ /** 指定スラッグの前後コンテンツを返す。 */
208
+ getAdjacent(slug: string): Promise<{
209
+ prev: T | null;
210
+ next: T | null;
211
+ }>;
212
+ /** 全コンテンツを事前レンダリングしてキャッシュに保存する。 */
213
+ prefetchAll(opts?: {
214
+ concurrency?: number;
215
+ onProgress?: (done: number, total: number) => void;
216
+ }): Promise<{
217
+ ok: number;
218
+ failed: number;
219
+ }>;
220
+ /** 静的生成用のスラッグ一覧を返す。 */
221
+ getStaticSlugs(): Promise<string[]>;
222
+ /** 指定スコープのキャッシュを無効化する。 */
223
+ revalidate(scope?: "all" | {
224
+ slug: string;
225
+ }): Promise<void>;
226
+ /** Webhook ペイロードを元にキャッシュを同期する。 */
227
+ syncFromWebhook(payload?: {
228
+ slug?: string;
177
229
  }): Promise<{
230
+ updated: string[];
231
+ }>;
232
+ /** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
233
+ getList(): Promise<{
178
234
  items: T[];
179
235
  listVersion: string;
180
236
  }>;
181
- getItemCachedFirst(slug: string, options?: {
182
- waitUntil?: (promise: Promise<void>) => void;
183
- }): Promise<CachedItem<T> | null>;
184
- checkItemsUpdate(clientVersion: string): Promise<{
237
+ /** キャッシュ優先で単一コンテンツを返す(SWR)。 */
238
+ getItem(slug: string): Promise<CachedItem<T> | null>;
239
+ checkListUpdate(version: string): Promise<{
185
240
  changed: false;
186
241
  } | {
187
242
  changed: true;
@@ -195,10 +250,14 @@ declare class CMS<T extends BaseContentItem = BaseContentItem> {
195
250
  item: T;
196
251
  notionUpdatedAt: string;
197
252
  }>;
253
+ /** ハッシュキーでキャッシュ画像を取得する。 */
254
+ getCachedImage(hash: string): Promise<StorageBinary | null>;
255
+ /** ハッシュキーでキャッシュ画像を Response として返す。 */
256
+ createCachedImageResponse(hash: string): Promise<Response | null>;
198
257
  private buildCachedItem;
199
258
  }
200
- /** 設定済みのCMSインスタンスを生成するファクトリ関数。 */
201
- declare function createCMS<T extends BaseContentItem = BaseContentItem>(config?: CMSConfig<T>): CMS<T>;
259
+ /** 設定済みの CMS インスタンスを生成するファクトリ関数。 */
260
+ declare function createCMS<T extends BaseContentItem = BaseContentItem>(opts: CreateCMSOptions<T>): CMS<T>;
202
261
 
203
262
  type CMSErrorCode = "CONFIG_INVALID" | "NOTION_ITEM_SCHEMA_INVALID" | "NOTION_FETCH_ITEMS_FAILED" | "NOTION_FETCH_ITEM_BY_SLUG_FAILED" | "NOTION_GET_BLOCKS_FAILED" | "NOTION_MARKDOWN_FETCH_FAILED" | "IMAGE_CACHE_FAILED" | "RENDERER_FAILED";
204
263
  interface CMSErrorContext {
@@ -230,4 +289,4 @@ declare function getPlainText(items: RichTextItemResponse[] | undefined): string
230
289
  */
231
290
  declare function mapItem(page: PageObjectResponse, props: Required<CMSSchemaProperties>): BaseContentItem;
232
291
 
233
- export { type BaseContentItem, CMS, type CMSConfig, type CMSEnv, CMSError, type CMSErrorCode, type CMSErrorContext, type CMSSchemaProperties, CacheStore, type CachedItem, type CachedItemList, type StorageAdapter, type StorageBinary, createCMS, getPlainText, isCMSError, isStale, mapItem, sha256Hex };
292
+ export { type BaseContentItem, CMS, CMSError, type CMSErrorCode, type CMSErrorContext, type CMSSchemaProperties, type CacheConfig, type CachedItem, type CachedItemList, type ContentConfig, type CreateCMSOptions, type DataSourceAdapter, type DocumentCacheAdapter, type ImageCacheAdapter, type SchemaConfig, type StorageBinary, createCMS, getPlainText, isCMSError, isStale, mapItem, memoryCache, memoryDocumentCache, memoryImageCache, noopDocumentCache, noopImageCache, sha256Hex };
package/dist/index.js CHANGED
@@ -8,58 +8,95 @@ function isStale(cachedAt, ttlMs) {
8
8
  if (ttlMs === void 0) return false;
9
9
  return Date.now() - cachedAt > ttlMs;
10
10
  }
11
- var CacheStore = class {
12
- storage;
13
- listKey;
14
- itemPrefix;
15
- imagePrefix;
16
- constructor(storage, listKey, itemPrefix, imagePrefix) {
17
- this.storage = storage;
18
- this.listKey = listKey;
19
- this.itemPrefix = itemPrefix;
20
- this.imagePrefix = imagePrefix;
21
- }
22
- getItemList() {
23
- if (!this.storage) return Promise.resolve(null);
24
- return this.storage.json(this.listKey);
25
- }
26
- async setItemList(items) {
27
- if (!this.storage) return;
28
- const data = { items, cachedAt: Date.now() };
29
- await this.storage.put(this.listKey, JSON.stringify(data), {
30
- contentType: "application/json"
31
- });
11
+
12
+ // src/cache/memory.ts
13
+ var MemoryDocumentCache = class {
14
+ name = "memory-document";
15
+ list = null;
16
+ items = /* @__PURE__ */ new Map();
17
+ getList() {
18
+ return Promise.resolve(this.list);
19
+ }
20
+ setList(data) {
21
+ this.list = data;
22
+ return Promise.resolve();
32
23
  }
33
24
  getItem(slug) {
34
- if (!this.storage) return Promise.resolve(null);
35
- return this.storage.json(`${this.itemPrefix}${slug}.json`);
36
- }
37
- async setItem(slug, data) {
38
- if (!this.storage) return;
39
- await this.storage.put(
40
- `${this.itemPrefix}${slug}.json`,
41
- JSON.stringify(data),
42
- { contentType: "application/json" }
43
- );
25
+ return Promise.resolve(this.items.get(slug) ?? null);
26
+ }
27
+ setItem(slug, data) {
28
+ this.items.set(slug, data);
29
+ return Promise.resolve();
44
30
  }
45
- getImage(hash) {
46
- if (!this.storage) return Promise.resolve(null);
47
- return this.storage.binary(`${this.imagePrefix}${hash}`);
31
+ async invalidate(scope) {
32
+ if (scope === "all") {
33
+ this.list = null;
34
+ this.items.clear();
35
+ } else if ("slug" in scope) {
36
+ this.items.delete(scope.slug);
37
+ }
38
+ }
39
+ };
40
+ var MemoryImageCache = class {
41
+ name = "memory-image";
42
+ store = /* @__PURE__ */ new Map();
43
+ get(hash) {
44
+ return Promise.resolve(this.store.get(hash) ?? null);
45
+ }
46
+ set(hash, data, contentType) {
47
+ this.store.set(hash, { data, contentType });
48
+ return Promise.resolve();
49
+ }
50
+ };
51
+ function memoryDocumentCache() {
52
+ return new MemoryDocumentCache();
53
+ }
54
+ function memoryImageCache() {
55
+ return new MemoryImageCache();
56
+ }
57
+ function memoryCache() {
58
+ return new MemoryDocumentCache();
59
+ }
60
+
61
+ // src/cache/noop.ts
62
+ var NoopDocumentCache = class {
63
+ name = "noop-document";
64
+ getList() {
65
+ return Promise.resolve(null);
66
+ }
67
+ setList(_data) {
68
+ return Promise.resolve();
69
+ }
70
+ getItem(_slug) {
71
+ return Promise.resolve(null);
72
+ }
73
+ setItem(_slug, _data) {
74
+ return Promise.resolve();
75
+ }
76
+ invalidate(_scope) {
77
+ return Promise.resolve();
78
+ }
79
+ };
80
+ var NoopImageCache = class {
81
+ name = "noop-image";
82
+ get(_hash) {
83
+ return Promise.resolve(null);
48
84
  }
49
- async setImage(hash, data, contentType) {
50
- if (!this.storage) return;
51
- await this.storage.put(`${this.imagePrefix}${hash}`, data, { contentType });
85
+ set(_hash, _data, _contentType) {
86
+ return Promise.resolve();
52
87
  }
53
88
  };
89
+ var _noopDocument = new NoopDocumentCache();
90
+ var _noopImage = new NoopImageCache();
91
+ function noopDocumentCache() {
92
+ return _noopDocument;
93
+ }
94
+ function noopImageCache() {
95
+ return _noopImage;
96
+ }
54
97
 
55
98
  // src/cms.ts
56
- import {
57
- createClient,
58
- queryAllPages,
59
- queryPageBySlug
60
- } from "@notion-headless-cms/fetcher";
61
99
  import { renderMarkdown } from "@notion-headless-cms/renderer";
62
- import { Transformer } from "@notion-headless-cms/transformer";
63
100
 
64
101
  // src/errors.ts
65
102
  var CMSError = class extends Error {
@@ -88,10 +125,10 @@ function inferContentType(url, responseContentType) {
88
125
  if (url.includes(".webp")) return "image/webp";
89
126
  return "image/jpeg";
90
127
  }
91
- async function fetchAndCacheImage(store, notionUrl, imageProxyBase) {
128
+ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
92
129
  const hash = await sha256Hex(notionUrl);
93
130
  const proxyUrl = `${imageProxyBase}/${hash}`;
94
- const existing = await store.getImage(hash);
131
+ const existing = await cache.get(hash);
95
132
  if (existing) return proxyUrl;
96
133
  try {
97
134
  const response = await fetch(notionUrl, {
@@ -103,7 +140,7 @@ async function fetchAndCacheImage(store, notionUrl, imageProxyBase) {
103
140
  notionUrl,
104
141
  response.headers.get("content-type")
105
142
  );
106
- await store.setImage(hash, data, contentType);
143
+ await cache.set(hash, data, contentType);
107
144
  } catch (err) {
108
145
  throw new CMSError({
109
146
  code: "IMAGE_CACHE_FAILED",
@@ -114,282 +151,228 @@ async function fetchAndCacheImage(store, notionUrl, imageProxyBase) {
114
151
  }
115
152
  return proxyUrl;
116
153
  }
117
- function buildCacheImageFn(store, imageProxyBase) {
118
- return (notionUrl) => fetchAndCacheImage(store, notionUrl, imageProxyBase);
119
- }
120
-
121
- // src/mapper.ts
122
- import { z } from "zod";
123
- var baseContentItemSchema = z.object({
124
- id: z.string().min(1),
125
- slug: z.string(),
126
- status: z.string(),
127
- publishedAt: z.string().min(1),
128
- updatedAt: z.string().min(1)
129
- });
130
- function getPlainText(items) {
131
- return items?.map((item) => item.plain_text).join("") ?? "";
132
- }
133
- function mapItem(page, props) {
134
- const statusProperty = page.properties[props.status];
135
- const dateProperty = page.properties[props.date];
136
- const parsed = baseContentItemSchema.safeParse({
137
- id: page.id,
138
- slug: getPlainText(
139
- page.properties[props.slug]?.rich_text
140
- ),
141
- status: statusProperty?.status?.name ?? statusProperty?.select?.name ?? "",
142
- publishedAt: dateProperty?.date?.start ?? page.created_time,
143
- updatedAt: page.last_edited_time
144
- });
145
- if (!parsed.success) {
146
- throw new CMSError({
147
- code: "NOTION_ITEM_SCHEMA_INVALID",
148
- message: "Failed to parse Notion page into BaseContentItem.",
149
- context: {
150
- operation: "mapItem",
151
- pageId: page.id,
152
- issues: JSON.stringify(parsed.error.issues)
153
- }
154
- });
155
- }
156
- return parsed.data;
154
+ function buildCacheImageFn(cache, imageProxyBase) {
155
+ return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase);
157
156
  }
158
157
 
159
158
  // src/cms.ts
160
- var DEFAULT_PROPERTIES = {
161
- slug: "Slug",
162
- status: "Status",
163
- date: "CreatedAt"
164
- };
165
159
  var DEFAULT_IMAGE_PROXY_BASE = "/api/images";
166
- var DEFAULT_LIST_KEY = "content.json";
167
- var DEFAULT_ITEM_PREFIX = "content/";
168
- var DEFAULT_IMAGE_PREFIX = "images/";
169
160
  function buildListVersion(items) {
170
161
  return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
171
162
  }
172
- function validateEnv(env) {
173
- if (!env.NOTION_TOKEN || !env.NOTION_DATA_SOURCE_ID) {
174
- throw new CMSError({
175
- code: "CONFIG_INVALID",
176
- message: "NOTION_TOKEN and NOTION_DATA_SOURCE_ID are required to use Notion CMS APIs.",
177
- context: {
178
- operation: "validateEnv",
179
- hasNotionToken: !!env.NOTION_TOKEN,
180
- hasNotionDataSourceId: !!env.NOTION_DATA_SOURCE_ID
181
- }
182
- });
163
+ function resolveDocumentCache(cache) {
164
+ if (!cache || cache.document === false || cache.document === void 0) {
165
+ return noopDocumentCache();
183
166
  }
184
- return { dataSourceId: env.NOTION_DATA_SOURCE_ID };
167
+ return cache.document;
168
+ }
169
+ function resolveImageCache(cache) {
170
+ if (!cache || cache.image === false || cache.image === void 0) {
171
+ return noopImageCache();
172
+ }
173
+ return cache.image;
185
174
  }
186
175
  var CMS = class {
187
- itemMapper;
188
- slugPropName;
176
+ source;
177
+ docCache;
178
+ imgCache;
179
+ hasImageCache;
180
+ ttlMs;
189
181
  publishedStatuses;
190
182
  accessibleStatuses;
191
183
  imageProxyBase;
192
- transformerConfig;
193
- rendererConfig;
194
- store;
195
- hasStorage;
196
- ttlMs;
197
- client;
198
- dataSourceId;
199
- constructor(config) {
200
- const props = {
201
- ...DEFAULT_PROPERTIES,
202
- ...config?.schema?.properties
203
- };
204
- this.slugPropName = props.slug;
205
- if (config?.schema?.mapItem) {
206
- this.itemMapper = config.schema.mapItem;
207
- } else {
208
- this.itemMapper = (page) => mapItem(page, props);
209
- }
210
- this.publishedStatuses = config?.schema?.publishedStatuses ?? [];
211
- this.accessibleStatuses = config?.schema?.accessibleStatuses ?? [];
212
- this.imageProxyBase = config?.renderer?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
213
- this.transformerConfig = config?.transformer;
214
- this.rendererConfig = config?.renderer;
215
- this.hasStorage = !!config?.storage;
216
- this.ttlMs = config?.cache?.ttlMs;
217
- this.store = new CacheStore(
218
- config?.storage,
219
- config?.cache?.listKey ?? DEFAULT_LIST_KEY,
220
- config?.cache?.itemPrefix ?? DEFAULT_ITEM_PREFIX,
221
- config?.cache?.imagePrefix ?? DEFAULT_IMAGE_PREFIX
222
- );
223
- if (config?.env) {
224
- const { dataSourceId } = validateEnv(config.env);
225
- this.client = createClient(config.env);
226
- this.dataSourceId = dataSourceId;
227
- }
228
- }
229
- // ── プライベートヘルパー(認証) ──────────────────────────────────────
230
- requireClient() {
231
- if (!this.client || !this.dataSourceId) {
232
- throw new CMSError({
233
- code: "CONFIG_INVALID",
234
- message: "NOTION_TOKEN \u3068 NOTION_DATA_SOURCE_ID \u306F CMS \u306E\u8A2D\u5B9A\u306B\u5FC5\u8981\u3067\u3059\u3002",
235
- context: { operation: "requireClient" }
236
- });
237
- }
238
- return { client: this.client, dataSourceId: this.dataSourceId };
184
+ contentConfig;
185
+ waitUntil;
186
+ constructor(opts) {
187
+ this.source = opts.source;
188
+ this.docCache = resolveDocumentCache(opts.cache);
189
+ this.imgCache = resolveImageCache(opts.cache);
190
+ this.hasImageCache = !!opts.cache?.image;
191
+ this.ttlMs = opts.cache?.ttlMs;
192
+ this.publishedStatuses = opts.schema?.publishedStatuses ?? (opts.source.publishedStatuses ? [...opts.source.publishedStatuses] : []);
193
+ this.accessibleStatuses = opts.schema?.accessibleStatuses ?? (opts.source.accessibleStatuses ? [...opts.source.accessibleStatuses] : []);
194
+ this.imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
195
+ this.contentConfig = opts.content;
196
+ this.waitUntil = opts.waitUntil;
239
197
  }
240
- // ── コンテンツ取得 ────────────────────────────────────────────────────
241
- /** 公開済みコンテンツ一覧を Notion から直接取得する。 */
242
- async getItems() {
243
- const { client, dataSourceId } = this.requireClient();
244
- try {
245
- const pages = await queryAllPages(client, dataSourceId);
246
- const items = pages.map(this.itemMapper);
247
- const filtered = this.publishedStatuses.length > 0 ? items.filter((item) => this.publishedStatuses.includes(item.status)) : items;
248
- return filtered.sort(
249
- (a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
250
- );
251
- } catch (err) {
252
- if (isCMSError(err)) throw err;
253
- throw new CMSError({
254
- code: "NOTION_FETCH_ITEMS_FAILED",
255
- message: "Failed to fetch items from Notion data source.",
256
- cause: err,
257
- context: { operation: "getItems", dataSourceId }
258
- });
259
- }
198
+ // ── コンテンツ取得 ──────────────────────────────────────────────────────
199
+ /** 公開済みコンテンツ一覧をソースから直接取得する。 */
200
+ list() {
201
+ return this.source.list({
202
+ publishedStatuses: this.publishedStatuses.length > 0 ? this.publishedStatuses : void 0
203
+ });
260
204
  }
261
- /** スラッグでコンテンツを Notion から直接取得する。 */
262
- async getItemBySlug(slug) {
263
- const { client, dataSourceId } = this.requireClient();
264
- try {
265
- const page = await queryPageBySlug(
266
- client,
267
- dataSourceId,
268
- slug,
269
- this.slugPropName
270
- );
271
- if (!page) return null;
272
- const item = this.itemMapper(page);
273
- if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
274
- return null;
275
- }
276
- return item;
277
- } catch (err) {
278
- if (isCMSError(err)) throw err;
279
- throw new CMSError({
280
- code: "NOTION_FETCH_ITEM_BY_SLUG_FAILED",
281
- message: "Failed to fetch item by slug from Notion data source.",
282
- cause: err,
283
- context: { operation: "getItemBySlug", dataSourceId, slug }
284
- });
205
+ /** スラッグでコンテンツをソースから直接取得する。 */
206
+ async findBySlug(slug) {
207
+ const item = await this.source.findBySlug(slug);
208
+ if (!item) return null;
209
+ if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
210
+ return null;
285
211
  }
212
+ return item;
286
213
  }
287
214
  /** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
288
215
  isPublished(item) {
289
216
  if (this.publishedStatuses.length === 0) return true;
290
217
  return this.publishedStatuses.includes(item.status);
291
218
  }
292
- /** コンテンツをMarkdown→HTMLにレンダリングし、CachedItemとして返す。 */
293
- async renderItem(item) {
294
- const { client } = this.requireClient();
295
- return this.buildCachedItem(client, item);
219
+ /** コンテンツを Markdown HTML にレンダリングし、CachedItem として返す。 */
220
+ async render(item) {
221
+ return this.buildCachedItem(item);
296
222
  }
297
- /** スラッグでコンテンツを取得してMarkdown→HTMLにレンダリングする。 */
298
- async renderItemBySlug(slug) {
299
- const { client, dataSourceId } = this.requireClient();
223
+ /** スラッグでコンテンツを取得して Markdown HTML にレンダリングする。 */
224
+ async renderBySlug(slug) {
300
225
  try {
301
- const page = await queryPageBySlug(
302
- client,
303
- dataSourceId,
304
- slug,
305
- this.slugPropName
306
- );
307
- if (!page) return null;
308
- const item = this.itemMapper(page);
309
- if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
310
- return null;
311
- }
312
- return this.buildCachedItem(client, item);
226
+ const item = await this.findBySlug(slug);
227
+ if (!item) return null;
228
+ return this.buildCachedItem(item);
313
229
  } catch (err) {
314
230
  if (isCMSError(err)) throw err;
315
231
  throw new CMSError({
316
232
  code: "NOTION_FETCH_ITEM_BY_SLUG_FAILED",
317
233
  message: "Failed to fetch item by slug from Notion data source.",
318
234
  cause: err,
319
- context: { operation: "renderItemBySlug", dataSourceId, slug }
235
+ context: { operation: "renderBySlug", slug }
320
236
  });
321
237
  }
322
238
  }
323
- // ── キャッシュ操作 ─────────────────────────────────────────────────────
324
- getCachedItemList() {
325
- return this.store.getItemList();
239
+ // ── 便利 API ───────────────────────────────────────────────────────────
240
+ /** ステータスでフィルタリングした一覧を返す。 */
241
+ async listByStatus(status) {
242
+ const statuses = Array.isArray(status) ? status : [status];
243
+ const all = await this.source.list();
244
+ return all.filter((item) => statuses.includes(item.status));
326
245
  }
327
- setCachedItemList(items) {
328
- return this.store.setItemList(items);
246
+ /** 任意の条件でフィルタリングした一覧を返す。 */
247
+ async where(predicate) {
248
+ const items = await this.list();
249
+ return items.filter(predicate);
329
250
  }
330
- getCachedItem(slug) {
331
- return this.store.getItem(slug);
251
+ /** ページネーション付き一覧を返す。 */
252
+ async paginate(opts) {
253
+ const all = await this.list();
254
+ const total = all.length;
255
+ const start = (opts.page - 1) * opts.perPage;
256
+ const items = all.slice(start, start + opts.perPage);
257
+ return {
258
+ items,
259
+ total,
260
+ page: opts.page,
261
+ perPage: opts.perPage,
262
+ hasNext: start + opts.perPage < total
263
+ };
332
264
  }
333
- setCachedItem(slug, data) {
334
- return this.store.setItem(slug, data);
265
+ /** 指定スラッグの前後コンテンツを返す。 */
266
+ async getAdjacent(slug) {
267
+ const items = await this.list();
268
+ const idx = items.findIndex((item) => item.slug === slug);
269
+ if (idx === -1) return { prev: null, next: null };
270
+ return {
271
+ prev: idx > 0 ? items[idx - 1] : null,
272
+ next: idx < items.length - 1 ? items[idx + 1] : null
273
+ };
335
274
  }
336
- getCachedImage(hash) {
337
- return this.store.getImage(hash);
275
+ /** 全コンテンツを事前レンダリングしてキャッシュに保存する。 */
276
+ async prefetchAll(opts) {
277
+ const items = await this.list();
278
+ const concurrency = opts?.concurrency ?? 3;
279
+ let ok = 0;
280
+ let failed = 0;
281
+ for (let i = 0; i < items.length; i += concurrency) {
282
+ const chunk = items.slice(i, i + concurrency);
283
+ await Promise.all(
284
+ chunk.map(async (item) => {
285
+ try {
286
+ const rendered = await this.buildCachedItem(item);
287
+ await this.docCache.setItem(item.slug, rendered);
288
+ ok++;
289
+ } catch {
290
+ failed++;
291
+ }
292
+ })
293
+ );
294
+ opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);
295
+ }
296
+ await this.docCache.setList({ items, cachedAt: Date.now() });
297
+ return { ok, failed };
338
298
  }
339
- async createCachedImageResponse(hash) {
340
- const object = await this.store.getImage(hash);
341
- if (!object) return null;
342
- const headers = new Headers();
343
- if (object.contentType) headers.set("content-type", object.contentType);
344
- headers.set("cache-control", "public, max-age=31536000, immutable");
345
- return new Response(object.data, { headers });
299
+ /** 静的生成用のスラッグ一覧を返す。 */
300
+ async getStaticSlugs() {
301
+ const items = await this.list();
302
+ return items.map((item) => item.slug);
303
+ }
304
+ /** 指定スコープのキャッシュを無効化する。 */
305
+ async revalidate(scope) {
306
+ if (!this.docCache.invalidate) return;
307
+ await this.docCache.invalidate(scope ?? "all");
308
+ }
309
+ /** Webhook ペイロードを元にキャッシュを同期する。 */
310
+ async syncFromWebhook(payload) {
311
+ const updated = [];
312
+ if (payload?.slug) {
313
+ const item = await this.findBySlug(payload.slug);
314
+ if (item) {
315
+ const rendered = await this.buildCachedItem(item);
316
+ await this.docCache.setItem(item.slug, rendered);
317
+ updated.push(item.slug);
318
+ }
319
+ } else {
320
+ const result = await this.prefetchAll();
321
+ if (result.ok > 0) {
322
+ const items = await this.list();
323
+ for (const item of items) updated.push(item.slug);
324
+ }
325
+ }
326
+ return { updated };
346
327
  }
347
328
  // ── キャッシュ優先取得(Stale-While-Revalidate) ─────────────────────
348
- async getItemsCachedFirst(options) {
349
- const cached = await this.store.getItemList();
329
+ /** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
330
+ async getList() {
331
+ const cached = await this.docCache.getList();
350
332
  if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
351
333
  return {
352
334
  items: cached.items,
353
335
  listVersion: buildListVersion(cached.items)
354
336
  };
355
337
  }
356
- const items = await this.getItems();
357
- const save = this.store.setItemList(items);
358
- if (options?.waitUntil) {
359
- options.waitUntil(save);
338
+ const items = await this.list();
339
+ const save = this.docCache.setList({ items, cachedAt: Date.now() });
340
+ if (this.waitUntil) {
341
+ this.waitUntil(save);
360
342
  } else {
361
343
  await save;
362
344
  }
363
345
  return { items, listVersion: buildListVersion(items) };
364
346
  }
365
- async getItemCachedFirst(slug, options) {
366
- const cached = await this.store.getItem(slug);
347
+ /** キャッシュ優先で単一コンテンツを返す(SWR)。 */
348
+ async getItem(slug) {
349
+ const cached = await this.docCache.getItem(slug);
367
350
  if (cached && !isStale(cached.cachedAt, this.ttlMs)) return cached;
368
- const entry = await this.renderItemBySlug(slug);
351
+ const entry = await this.renderBySlug(slug);
369
352
  if (!entry) return null;
370
- const save = this.store.setItem(slug, entry);
371
- if (options?.waitUntil) {
372
- options.waitUntil(save);
353
+ const save = this.docCache.setItem(slug, entry);
354
+ if (this.waitUntil) {
355
+ this.waitUntil(save);
373
356
  } else {
374
357
  await save;
375
358
  }
376
359
  return entry;
377
360
  }
378
- async checkItemsUpdate(clientVersion) {
379
- const items = await this.getItems();
361
+ async checkListUpdate(version) {
362
+ const items = await this.list();
380
363
  const serverVersion = buildListVersion(items);
381
- if (serverVersion === clientVersion) return { changed: false };
382
- await this.store.setItemList(items);
364
+ if (serverVersion === version) return { changed: false };
365
+ await this.docCache.setList({ items, cachedAt: Date.now() });
383
366
  return { changed: true, items };
384
367
  }
385
368
  async checkItemUpdate(slug, lastEdited) {
386
- const item = await this.getItemBySlug(slug);
369
+ const item = await this.findBySlug(slug);
387
370
  if (!item) return { changed: false };
388
371
  if (!this.isPublished(item)) return { changed: false };
389
372
  if (item.updatedAt === lastEdited) return { changed: false };
390
- const entry = await this.renderItemBySlug(slug);
373
+ const entry = await this.renderBySlug(slug);
391
374
  if (!entry) return { changed: false };
392
- await this.store.setItem(slug, entry);
375
+ await this.docCache.setItem(slug, entry);
393
376
  return {
394
377
  changed: true,
395
378
  html: entry.html,
@@ -397,36 +380,47 @@ var CMS = class {
397
380
  notionUpdatedAt: entry.notionUpdatedAt
398
381
  };
399
382
  }
400
- // ── プライベートヘルパー ───────────────────────────────────────────────
401
- async buildCachedItem(client, item) {
402
- const transformer = new Transformer(
403
- this.transformerConfig?.blocks ? { blocks: this.transformerConfig.blocks } : void 0
404
- );
383
+ // ── 画像配信 ───────────────────────────────────────────────────────────
384
+ /** ハッシュキーでキャッシュ画像を取得する。 */
385
+ getCachedImage(hash) {
386
+ return this.imgCache.get(hash);
387
+ }
388
+ /** ハッシュキーでキャッシュ画像を Response として返す。 */
389
+ async createCachedImageResponse(hash) {
390
+ const object = await this.imgCache.get(hash);
391
+ if (!object) return null;
392
+ const headers = new Headers();
393
+ if (object.contentType) headers.set("content-type", object.contentType);
394
+ headers.set("cache-control", "public, max-age=31536000, immutable");
395
+ return new Response(object.data, { headers });
396
+ }
397
+ // ── プライベートヘルパー ────────────────────────────────────────────────
398
+ async buildCachedItem(item) {
405
399
  let markdown;
406
400
  try {
407
- markdown = await transformer.transform(client, item.id);
401
+ markdown = await this.source.loadMarkdown(item);
408
402
  } catch (err) {
409
403
  if (isCMSError(err)) throw err;
410
404
  throw new CMSError({
411
405
  code: "NOTION_MARKDOWN_FETCH_FAILED",
412
- message: "Failed to load markdown from Notion.",
406
+ message: "Failed to load markdown from source.",
413
407
  cause: err,
414
408
  context: {
415
- operation: "buildCachedItem:transform",
409
+ operation: "buildCachedItem:loadMarkdown",
416
410
  pageId: item.id,
417
411
  slug: item.slug
418
412
  }
419
413
  });
420
414
  }
421
- const cacheImage = this.hasStorage ? buildCacheImageFn(this.store, this.imageProxyBase) : void 0;
415
+ const cacheImage = this.hasImageCache ? buildCacheImageFn(this.imgCache, this.imageProxyBase) : void 0;
422
416
  let html;
423
417
  try {
424
418
  html = await renderMarkdown(markdown, {
425
419
  imageProxyBase: this.imageProxyBase,
426
420
  cacheImage,
427
- remarkPlugins: this.rendererConfig?.remarkPlugins,
428
- rehypePlugins: this.rendererConfig?.rehypePlugins,
429
- render: this.rendererConfig?.render
421
+ remarkPlugins: this.contentConfig?.remarkPlugins,
422
+ rehypePlugins: this.contentConfig?.rehypePlugins,
423
+ render: this.contentConfig?.render
430
424
  });
431
425
  } catch (err) {
432
426
  if (isCMSError(err)) throw err;
@@ -449,17 +443,59 @@ var CMS = class {
449
443
  };
450
444
  }
451
445
  };
452
- function createCMS(config) {
453
- return new CMS(config);
446
+ function createCMS(opts) {
447
+ return new CMS(opts);
448
+ }
449
+
450
+ // src/mapper.ts
451
+ import { z } from "zod";
452
+ var baseContentItemSchema = z.object({
453
+ id: z.string().min(1),
454
+ slug: z.string(),
455
+ status: z.string(),
456
+ publishedAt: z.string().min(1),
457
+ updatedAt: z.string().min(1)
458
+ });
459
+ function getPlainText(items) {
460
+ return items?.map((item) => item.plain_text).join("") ?? "";
461
+ }
462
+ function mapItem(page, props) {
463
+ const statusProperty = page.properties[props.status];
464
+ const dateProperty = page.properties[props.date];
465
+ const parsed = baseContentItemSchema.safeParse({
466
+ id: page.id,
467
+ slug: getPlainText(
468
+ page.properties[props.slug]?.rich_text
469
+ ),
470
+ status: statusProperty?.status?.name ?? statusProperty?.select?.name ?? "",
471
+ publishedAt: dateProperty?.date?.start ?? page.created_time,
472
+ updatedAt: page.last_edited_time
473
+ });
474
+ if (!parsed.success) {
475
+ throw new CMSError({
476
+ code: "NOTION_ITEM_SCHEMA_INVALID",
477
+ message: "Failed to parse Notion page into BaseContentItem.",
478
+ context: {
479
+ operation: "mapItem",
480
+ pageId: page.id,
481
+ issues: JSON.stringify(parsed.error.issues)
482
+ }
483
+ });
484
+ }
485
+ return parsed.data;
454
486
  }
455
487
  export {
456
488
  CMS,
457
489
  CMSError,
458
- CacheStore,
459
490
  createCMS,
460
491
  getPlainText,
461
492
  isCMSError,
462
493
  isStale,
463
494
  mapItem,
495
+ memoryCache,
496
+ memoryDocumentCache,
497
+ memoryImageCache,
498
+ noopDocumentCache,
499
+ noopImageCache,
464
500
  sha256Hex
465
501
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notion-headless-cms/core",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Core CMS engine for notion-headless-cms — fetch, transform, cache with stale-while-revalidate strategy",
5
5
  "keywords": [
6
6
  "notion",