@notion-headless-cms/core 0.0.7 → 0.1.1
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/dist/index.d.ts +211 -29
- package/dist/index.js +342 -77
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,51 @@ interface CacheConfig<T extends BaseContentItem = BaseContentItem> {
|
|
|
83
83
|
ttlMs?: number;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
87
|
+
interface CMSHooks<T extends BaseContentItem = BaseContentItem> {
|
|
88
|
+
beforeCache?: (item: CachedItem<T>) => MaybePromise<CachedItem<T>>;
|
|
89
|
+
afterRender?: (html: string, item: T) => MaybePromise<string>;
|
|
90
|
+
onCacheHit?: (slug: string, item: CachedItem<T>) => void;
|
|
91
|
+
onCacheMiss?: (slug: string) => void;
|
|
92
|
+
onListCacheHit?: (items: T[], cachedAt: number) => void;
|
|
93
|
+
onListCacheMiss?: () => void;
|
|
94
|
+
onError?: (error: Error) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface Logger {
|
|
98
|
+
debug?: (message: string, context?: Record<string, unknown>) => void;
|
|
99
|
+
info?: (message: string, context?: Record<string, unknown>) => void;
|
|
100
|
+
warn?: (message: string, context?: Record<string, unknown>) => void;
|
|
101
|
+
error?: (message: string, context?: Record<string, unknown>) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface CMSPlugin<T extends BaseContentItem = BaseContentItem> {
|
|
105
|
+
name: string;
|
|
106
|
+
hooks?: CMSHooks<T>;
|
|
107
|
+
logger?: Partial<Logger>;
|
|
108
|
+
}
|
|
109
|
+
declare function definePlugin<T extends BaseContentItem>(plugin: CMSPlugin<T>): CMSPlugin<T>;
|
|
110
|
+
|
|
111
|
+
/** ソース側クエリのフィルタ・ソート条件。 */
|
|
112
|
+
interface SourceQueryOptions {
|
|
113
|
+
filter?: {
|
|
114
|
+
statuses?: string[];
|
|
115
|
+
tags?: string[];
|
|
116
|
+
[key: string]: unknown;
|
|
117
|
+
};
|
|
118
|
+
sort?: {
|
|
119
|
+
property: string;
|
|
120
|
+
direction: "asc" | "desc";
|
|
121
|
+
}[];
|
|
122
|
+
pageSize?: number;
|
|
123
|
+
cursor?: string;
|
|
124
|
+
}
|
|
125
|
+
/** ソース側クエリの結果。 */
|
|
126
|
+
interface SourceQueryResult<T> {
|
|
127
|
+
items: T[];
|
|
128
|
+
hasMore: boolean;
|
|
129
|
+
nextCursor?: string;
|
|
130
|
+
}
|
|
86
131
|
/**
|
|
87
132
|
* コンテンツソース(Notion など)を抽象化するインターフェース。
|
|
88
133
|
* core は Notion の知識を持たず、DataSourceAdapter 経由でのみデータを取得する。
|
|
@@ -96,6 +141,8 @@ interface DataSourceAdapter<T extends BaseContentItem = BaseContentItem> {
|
|
|
96
141
|
}): Promise<T[]>;
|
|
97
142
|
findBySlug(slug: string): Promise<T | null>;
|
|
98
143
|
loadMarkdown(item: T): Promise<string>;
|
|
144
|
+
/** Notion 側でフィルタ・ソートを行うクエリ(オプション)。 */
|
|
145
|
+
query?(opts: SourceQueryOptions): Promise<SourceQueryResult<T>>;
|
|
99
146
|
}
|
|
100
147
|
|
|
101
148
|
/** スキーマ設定。公開ステータスのフィルタやプロパティ名マッピングを制御する。 */
|
|
@@ -125,6 +172,17 @@ interface ContentConfig {
|
|
|
125
172
|
/** カスタムブロックハンドラーのマップ。Notionブロックタイプをキーとする。 */
|
|
126
173
|
blocks?: Record<string, BlockHandler>;
|
|
127
174
|
}
|
|
175
|
+
/** レートリミット・リトライ設定。 */
|
|
176
|
+
interface RateLimiterConfig {
|
|
177
|
+
/** 同時実行数の上限。デフォルト: 3 */
|
|
178
|
+
maxConcurrent?: number;
|
|
179
|
+
/** リトライ対象の HTTP ステータスコード。デフォルト: [429, 502, 503] */
|
|
180
|
+
retryOn?: number[];
|
|
181
|
+
/** 最大リトライ回数。デフォルト: 4 */
|
|
182
|
+
maxRetries?: number;
|
|
183
|
+
/** リトライ時の基準待機時間(ミリ秒)。デフォルト: 1000 */
|
|
184
|
+
baseDelayMs?: number;
|
|
185
|
+
}
|
|
128
186
|
/**
|
|
129
187
|
* createCMS() に渡すオプション。
|
|
130
188
|
* ジェネリクス型 T にカスタムコンテンツ型を指定できる(デフォルト: BaseContentItem)。
|
|
@@ -140,6 +198,14 @@ interface CreateCMSOptions<T extends BaseContentItem = BaseContentItem> {
|
|
|
140
198
|
content?: ContentConfig;
|
|
141
199
|
/** Cloudflare Workers の waitUntil に相当する非同期処理の登録関数。 */
|
|
142
200
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
201
|
+
/** ライフサイクルフック。 */
|
|
202
|
+
hooks?: CMSHooks<T>;
|
|
203
|
+
/** プラグイン配列。フックとロガーを組み合わせて提供できる。 */
|
|
204
|
+
plugins?: CMSPlugin<T>[];
|
|
205
|
+
/** ロガー。 */
|
|
206
|
+
logger?: Logger;
|
|
207
|
+
/** レートリミット・リトライ設定。 */
|
|
208
|
+
rateLimiter?: RateLimiterConfig;
|
|
143
209
|
}
|
|
144
210
|
|
|
145
211
|
/** インメモリキャッシュ(ドキュメント用)を生成する。 */
|
|
@@ -157,13 +223,88 @@ declare function noopDocumentCache<T extends BaseContentItem = BaseContentItem>(
|
|
|
157
223
|
/** 何もしない画像キャッシュを返す(シングルトン)。 */
|
|
158
224
|
declare function noopImageCache(): ImageCacheAdapter;
|
|
159
225
|
|
|
226
|
+
interface QueryResult<T> {
|
|
227
|
+
items: T[];
|
|
228
|
+
total: number;
|
|
229
|
+
page: number;
|
|
230
|
+
perPage: number;
|
|
231
|
+
hasNext: boolean;
|
|
232
|
+
hasPrev: boolean;
|
|
233
|
+
}
|
|
234
|
+
declare class QueryBuilder<T extends BaseContentItem> {
|
|
235
|
+
private readonly source;
|
|
236
|
+
private readonly defaultStatuses;
|
|
237
|
+
private _statuses;
|
|
238
|
+
private _tags;
|
|
239
|
+
private _predicate;
|
|
240
|
+
private _sortField;
|
|
241
|
+
private _sortDir;
|
|
242
|
+
private _page;
|
|
243
|
+
private _perPage;
|
|
244
|
+
constructor(source: DataSourceAdapter<T>, defaultStatuses?: string[]);
|
|
245
|
+
status(s: string | string[]): this;
|
|
246
|
+
tag(t: string | string[]): this;
|
|
247
|
+
where(predicate: (item: T) => boolean): this;
|
|
248
|
+
sortBy(field: keyof T & string, dir?: "asc" | "desc"): this;
|
|
249
|
+
paginate(opts: {
|
|
250
|
+
page: number;
|
|
251
|
+
perPage: number;
|
|
252
|
+
}): this;
|
|
253
|
+
execute(): Promise<QueryResult<T>>;
|
|
254
|
+
executeOne(): Promise<T | null>;
|
|
255
|
+
adjacent(slug: string): Promise<{
|
|
256
|
+
prev: T | null;
|
|
257
|
+
next: T | null;
|
|
258
|
+
}>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** キャッシュ優先アクセサ。 */
|
|
262
|
+
interface CachedAccessor<T extends BaseContentItem> {
|
|
263
|
+
list(): Promise<{
|
|
264
|
+
items: T[];
|
|
265
|
+
isStale: boolean;
|
|
266
|
+
cachedAt: number;
|
|
267
|
+
}>;
|
|
268
|
+
get(slug: string): Promise<CachedItem<T> | null>;
|
|
269
|
+
}
|
|
270
|
+
/** キャッシュ管理オペレーション。 */
|
|
271
|
+
interface CacheManager<T extends BaseContentItem> {
|
|
272
|
+
prefetchAll(opts?: {
|
|
273
|
+
concurrency?: number;
|
|
274
|
+
onProgress?: (done: number, total: number) => void;
|
|
275
|
+
}): Promise<{
|
|
276
|
+
ok: number;
|
|
277
|
+
failed: number;
|
|
278
|
+
}>;
|
|
279
|
+
revalidate(scope?: "all" | {
|
|
280
|
+
slug: string;
|
|
281
|
+
}): Promise<void>;
|
|
282
|
+
sync(payload?: {
|
|
283
|
+
slug?: string;
|
|
284
|
+
}): Promise<{
|
|
285
|
+
updated: string[];
|
|
286
|
+
}>;
|
|
287
|
+
checkList(version: string): Promise<{
|
|
288
|
+
changed: false;
|
|
289
|
+
} | {
|
|
290
|
+
changed: true;
|
|
291
|
+
items: T[];
|
|
292
|
+
}>;
|
|
293
|
+
checkItem(slug: string, lastEdited: string): Promise<{
|
|
294
|
+
changed: false;
|
|
295
|
+
} | {
|
|
296
|
+
changed: true;
|
|
297
|
+
html: string;
|
|
298
|
+
item: T;
|
|
299
|
+
notionUpdatedAt: string;
|
|
300
|
+
}>;
|
|
301
|
+
}
|
|
160
302
|
/**
|
|
161
303
|
* Notion をバックエンドとして使う汎用ヘッドレス CMS クラス。
|
|
162
304
|
*
|
|
163
305
|
* @example
|
|
164
306
|
* const cms = createCMS({
|
|
165
307
|
* source: notionAdapter({ token: '...', dataSourceId: '...' }),
|
|
166
|
-
* schema: { publishedStatuses: ['公開'] },
|
|
167
308
|
* });
|
|
168
309
|
* const items = await cms.list();
|
|
169
310
|
*/
|
|
@@ -178,37 +319,24 @@ declare class CMS<T extends BaseContentItem = BaseContentItem> {
|
|
|
178
319
|
private readonly imageProxyBase;
|
|
179
320
|
private readonly contentConfig;
|
|
180
321
|
private readonly waitUntil;
|
|
322
|
+
private readonly hooks;
|
|
323
|
+
private readonly logger;
|
|
324
|
+
private readonly retryConfig;
|
|
325
|
+
readonly cached: CachedAccessor<T>;
|
|
326
|
+
readonly cache: CacheManager<T>;
|
|
181
327
|
constructor(opts: CreateCMSOptions<T>);
|
|
182
328
|
/** 公開済みコンテンツ一覧をソースから直接取得する。 */
|
|
183
329
|
list(): Promise<T[]>;
|
|
184
330
|
/** スラッグでコンテンツをソースから直接取得する。 */
|
|
331
|
+
find(slug: string): Promise<T | null>;
|
|
332
|
+
/** @deprecated find() を使用してください。 */
|
|
185
333
|
findBySlug(slug: string): Promise<T | null>;
|
|
186
334
|
/** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
|
|
187
335
|
isPublished(item: T): boolean;
|
|
188
336
|
/** コンテンツを Markdown → HTML にレンダリングし、CachedItem として返す。 */
|
|
189
337
|
render(item: T): Promise<CachedItem<T>>;
|
|
190
|
-
/**
|
|
191
|
-
|
|
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
|
-
}>;
|
|
338
|
+
/** QueryBuilder を返す。ステータス・タグ・ページネーションなどを連鎖で指定できる。 */
|
|
339
|
+
query(): QueryBuilder<T>;
|
|
212
340
|
/** 全コンテンツを事前レンダリングしてキャッシュに保存する。 */
|
|
213
341
|
prefetchAll(opts?: {
|
|
214
342
|
concurrency?: number;
|
|
@@ -230,12 +358,9 @@ declare class CMS<T extends BaseContentItem = BaseContentItem> {
|
|
|
230
358
|
updated: string[];
|
|
231
359
|
}>;
|
|
232
360
|
/** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
|
|
233
|
-
|
|
234
|
-
items: T[];
|
|
235
|
-
listVersion: string;
|
|
236
|
-
}>;
|
|
361
|
+
private cachedList;
|
|
237
362
|
/** キャッシュ優先で単一コンテンツを返す(SWR)。 */
|
|
238
|
-
|
|
363
|
+
private cachedGet;
|
|
239
364
|
checkListUpdate(version: string): Promise<{
|
|
240
365
|
changed: false;
|
|
241
366
|
} | {
|
|
@@ -250,6 +375,41 @@ declare class CMS<T extends BaseContentItem = BaseContentItem> {
|
|
|
250
375
|
item: T;
|
|
251
376
|
notionUpdatedAt: string;
|
|
252
377
|
}>;
|
|
378
|
+
/** @deprecated cached.list() を使用してください。 */
|
|
379
|
+
getList(): Promise<{
|
|
380
|
+
items: T[];
|
|
381
|
+
listVersion: string;
|
|
382
|
+
}>;
|
|
383
|
+
/** @deprecated cached.get() を使用してください。 */
|
|
384
|
+
getItem(slug: string): Promise<CachedItem<T> | null>;
|
|
385
|
+
/** @deprecated cache.prefetchAll() を使用してください。 */
|
|
386
|
+
prefetchAllLegacy(opts?: {
|
|
387
|
+
concurrency?: number;
|
|
388
|
+
onProgress?: (done: number, total: number) => void;
|
|
389
|
+
}): Promise<{
|
|
390
|
+
ok: number;
|
|
391
|
+
failed: number;
|
|
392
|
+
}>;
|
|
393
|
+
/** @deprecated query().status(s).execute() を使用してください。 */
|
|
394
|
+
listByStatus(status: string | readonly string[]): Promise<T[]>;
|
|
395
|
+
/** @deprecated query().where(pred).execute() を使用してください。 */
|
|
396
|
+
where(predicate: (item: T) => boolean): Promise<T[]>;
|
|
397
|
+
/** @deprecated query().paginate(opts).execute() を使用してください。 */
|
|
398
|
+
paginate(opts: {
|
|
399
|
+
page: number;
|
|
400
|
+
perPage: number;
|
|
401
|
+
}): Promise<{
|
|
402
|
+
items: T[];
|
|
403
|
+
total: number;
|
|
404
|
+
page: number;
|
|
405
|
+
perPage: number;
|
|
406
|
+
hasNext: boolean;
|
|
407
|
+
}>;
|
|
408
|
+
/** @deprecated query().adjacent(slug) を使用してください。 */
|
|
409
|
+
getAdjacent(slug: string): Promise<{
|
|
410
|
+
prev: T | null;
|
|
411
|
+
next: T | null;
|
|
412
|
+
}>;
|
|
253
413
|
/** ハッシュキーでキャッシュ画像を取得する。 */
|
|
254
414
|
getCachedImage(hash: string): Promise<StorageBinary | null>;
|
|
255
415
|
/** ハッシュキーでキャッシュ画像を Response として返す。 */
|
|
@@ -280,6 +440,17 @@ declare class CMSError extends Error {
|
|
|
280
440
|
}
|
|
281
441
|
declare function isCMSError(error: unknown): error is CMSError;
|
|
282
442
|
|
|
443
|
+
/**
|
|
444
|
+
* プラグイン配列とダイレクトフックを合成して単一の CMSHooks を返す。
|
|
445
|
+
* beforeCache / afterRender はパイプライン(前の出力が次の入力)。
|
|
446
|
+
* onCacheHit などオブザーバー系は全員に同じ値を渡す。
|
|
447
|
+
*/
|
|
448
|
+
declare function mergeHooks<T extends BaseContentItem>(plugins: CMSPlugin<T>[], directHooks?: CMSHooks<T>): CMSHooks<T>;
|
|
449
|
+
/** プラグイン配列とダイレクトロガーを合成して単一の Logger を返す。 */
|
|
450
|
+
declare function mergeLoggers(plugins: Array<{
|
|
451
|
+
logger?: Partial<Logger>;
|
|
452
|
+
}>, directLogger?: Logger): Logger | undefined;
|
|
453
|
+
|
|
283
454
|
/** Notionリッチテキスト配列をプレーンテキストに結合する。 */
|
|
284
455
|
declare function getPlainText(items: RichTextItemResponse[] | undefined): string;
|
|
285
456
|
/**
|
|
@@ -289,4 +460,15 @@ declare function getPlainText(items: RichTextItemResponse[] | undefined): string
|
|
|
289
460
|
*/
|
|
290
461
|
declare function mapItem(page: PageObjectResponse, props: Required<CMSSchemaProperties>): BaseContentItem;
|
|
291
462
|
|
|
292
|
-
|
|
463
|
+
interface RetryConfig {
|
|
464
|
+
maxConcurrent: number;
|
|
465
|
+
retryOn: number[];
|
|
466
|
+
maxRetries: number;
|
|
467
|
+
baseDelayMs: number;
|
|
468
|
+
onRetry?: (attempt: number, status: number) => void;
|
|
469
|
+
}
|
|
470
|
+
declare const DEFAULT_RETRY_CONFIG: RetryConfig;
|
|
471
|
+
/** 指数バックオフでリトライする。retryOn に含まれる HTTP エラーのみ対象。 */
|
|
472
|
+
declare function withRetry<T>(fn: () => Promise<T>, config: RetryConfig): Promise<T>;
|
|
473
|
+
|
|
474
|
+
export { type BaseContentItem, CMS, CMSError, type CMSErrorCode, type CMSErrorContext, type CMSHooks, type CMSPlugin, type CMSSchemaProperties, type CacheConfig, type CachedItem, type CachedItemList, type ContentConfig, type CreateCMSOptions, DEFAULT_RETRY_CONFIG, type DataSourceAdapter, type DocumentCacheAdapter, type ImageCacheAdapter, type Logger, type MaybePromise, QueryBuilder, type QueryResult, type RateLimiterConfig, type RetryConfig, type SchemaConfig, type SourceQueryOptions, type SourceQueryResult, type StorageBinary, createCMS, definePlugin, getPlainText, isCMSError, isStale, mapItem, memoryCache, memoryDocumentCache, memoryImageCache, mergeHooks, mergeLoggers, noopDocumentCache, noopImageCache, sha256Hex, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -115,6 +115,72 @@ function isCMSError(error) {
|
|
|
115
115
|
return error instanceof CMSError;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// src/hooks.ts
|
|
119
|
+
function mergeHooks(plugins, directHooks) {
|
|
120
|
+
const allHooks = [
|
|
121
|
+
...plugins.map((p) => p.hooks ?? {}),
|
|
122
|
+
...directHooks ? [directHooks] : []
|
|
123
|
+
];
|
|
124
|
+
if (allHooks.length === 0) return {};
|
|
125
|
+
return {
|
|
126
|
+
beforeCache: buildPipeline(allHooks, "beforeCache"),
|
|
127
|
+
afterRender: buildRenderPipeline(allHooks),
|
|
128
|
+
onCacheHit: buildObserver(allHooks, "onCacheHit"),
|
|
129
|
+
onCacheMiss: buildObserver(allHooks, "onCacheMiss"),
|
|
130
|
+
onListCacheHit: buildObserver(allHooks, "onListCacheHit"),
|
|
131
|
+
onListCacheMiss: buildObserver(allHooks, "onListCacheMiss"),
|
|
132
|
+
onError: buildObserver(allHooks, "onError")
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function buildPipeline(hooks, key) {
|
|
136
|
+
const fns = hooks.map((h) => h[key]).filter(Boolean);
|
|
137
|
+
if (fns.length === 0) return void 0;
|
|
138
|
+
return async (item) => {
|
|
139
|
+
let current = item;
|
|
140
|
+
for (const fn of fns) {
|
|
141
|
+
current = await fn(current);
|
|
142
|
+
}
|
|
143
|
+
return current;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function buildRenderPipeline(hooks) {
|
|
147
|
+
const fns = hooks.map((h) => h.afterRender).filter(Boolean);
|
|
148
|
+
if (fns.length === 0) return void 0;
|
|
149
|
+
return async (html, item) => {
|
|
150
|
+
let current = html;
|
|
151
|
+
for (const fn of fns) {
|
|
152
|
+
current = await fn(current, item);
|
|
153
|
+
}
|
|
154
|
+
return current;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function buildObserver(hooks, key) {
|
|
158
|
+
const fns = hooks.map((h) => h[key]).filter(Boolean);
|
|
159
|
+
if (fns.length === 0) return void 0;
|
|
160
|
+
return ((...args) => {
|
|
161
|
+
for (const fn of fns) {
|
|
162
|
+
fn(...args);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function mergeLoggers(plugins, directLogger) {
|
|
167
|
+
const loggers = [
|
|
168
|
+
...plugins.map((p) => p.logger ?? {}),
|
|
169
|
+
...directLogger ? [directLogger] : []
|
|
170
|
+
];
|
|
171
|
+
if (loggers.length === 0) return void 0;
|
|
172
|
+
const merged = {};
|
|
173
|
+
for (const level of ["debug", "info", "warn", "error"]) {
|
|
174
|
+
const fns = loggers.map((l) => l[level]).filter(Boolean);
|
|
175
|
+
if (fns.length > 0) {
|
|
176
|
+
merged[level] = (message, context) => {
|
|
177
|
+
for (const fn of fns) fn(message, context);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return merged;
|
|
182
|
+
}
|
|
183
|
+
|
|
118
184
|
// src/image.ts
|
|
119
185
|
function inferContentType(url, responseContentType) {
|
|
120
186
|
if (responseContentType?.startsWith("image/")) {
|
|
@@ -155,11 +221,146 @@ function buildCacheImageFn(cache, imageProxyBase) {
|
|
|
155
221
|
return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase);
|
|
156
222
|
}
|
|
157
223
|
|
|
224
|
+
// src/query.ts
|
|
225
|
+
var QueryBuilder = class {
|
|
226
|
+
source;
|
|
227
|
+
defaultStatuses;
|
|
228
|
+
_statuses = [];
|
|
229
|
+
_tags = [];
|
|
230
|
+
_predicate;
|
|
231
|
+
_sortField;
|
|
232
|
+
_sortDir = "asc";
|
|
233
|
+
_page = 1;
|
|
234
|
+
_perPage = 20;
|
|
235
|
+
constructor(source, defaultStatuses = []) {
|
|
236
|
+
this.source = source;
|
|
237
|
+
this.defaultStatuses = defaultStatuses;
|
|
238
|
+
}
|
|
239
|
+
status(s) {
|
|
240
|
+
this._statuses = Array.isArray(s) ? s : [s];
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
tag(t) {
|
|
244
|
+
this._tags = Array.isArray(t) ? t : [t];
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
where(predicate) {
|
|
248
|
+
this._predicate = predicate;
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
sortBy(field, dir = "asc") {
|
|
252
|
+
this._sortField = field;
|
|
253
|
+
this._sortDir = dir;
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
paginate(opts) {
|
|
257
|
+
this._page = opts.page;
|
|
258
|
+
this._perPage = opts.perPage;
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
async execute() {
|
|
262
|
+
const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
|
|
263
|
+
if (this.source.query && !this._predicate) {
|
|
264
|
+
const result = await this.source.query({
|
|
265
|
+
filter: {
|
|
266
|
+
statuses,
|
|
267
|
+
tags: this._tags.length > 0 ? this._tags : void 0
|
|
268
|
+
},
|
|
269
|
+
sort: this._sortField ? [{ property: this._sortField, direction: this._sortDir }] : void 0,
|
|
270
|
+
pageSize: this._perPage
|
|
271
|
+
});
|
|
272
|
+
const items2 = result.items;
|
|
273
|
+
return {
|
|
274
|
+
items: items2,
|
|
275
|
+
total: items2.length,
|
|
276
|
+
page: this._page,
|
|
277
|
+
perPage: this._perPage,
|
|
278
|
+
hasNext: result.hasMore,
|
|
279
|
+
hasPrev: this._page > 1
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
let items = await this.source.list({
|
|
283
|
+
publishedStatuses: statuses
|
|
284
|
+
});
|
|
285
|
+
if (this._tags.length > 0) {
|
|
286
|
+
items = items.filter((item) => {
|
|
287
|
+
const itemTags = item.tags;
|
|
288
|
+
if (!Array.isArray(itemTags)) return false;
|
|
289
|
+
return this._tags.some((tag) => itemTags.includes(tag));
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (this._predicate) {
|
|
293
|
+
items = items.filter(this._predicate);
|
|
294
|
+
}
|
|
295
|
+
if (this._sortField) {
|
|
296
|
+
const field = this._sortField;
|
|
297
|
+
const dir = this._sortDir;
|
|
298
|
+
items = [...items].sort((a, b) => {
|
|
299
|
+
const av = a[field];
|
|
300
|
+
const bv = b[field];
|
|
301
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
302
|
+
return dir === "asc" ? cmp : -cmp;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const total = items.length;
|
|
306
|
+
const start = (this._page - 1) * this._perPage;
|
|
307
|
+
const paged = items.slice(start, start + this._perPage);
|
|
308
|
+
return {
|
|
309
|
+
items: paged,
|
|
310
|
+
total,
|
|
311
|
+
page: this._page,
|
|
312
|
+
perPage: this._perPage,
|
|
313
|
+
hasNext: start + this._perPage < total,
|
|
314
|
+
hasPrev: this._page > 1
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async executeOne() {
|
|
318
|
+
const result = await this.execute();
|
|
319
|
+
return result.items[0] ?? null;
|
|
320
|
+
}
|
|
321
|
+
async adjacent(slug) {
|
|
322
|
+
const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
|
|
323
|
+
const items = await this.source.list({ publishedStatuses: statuses });
|
|
324
|
+
const idx = items.findIndex((item) => item.slug === slug);
|
|
325
|
+
if (idx === -1) return { prev: null, next: null };
|
|
326
|
+
return {
|
|
327
|
+
prev: idx > 0 ? items[idx - 1] : null,
|
|
328
|
+
next: idx < items.length - 1 ? items[idx + 1] : null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/retry.ts
|
|
334
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
335
|
+
maxConcurrent: 3,
|
|
336
|
+
retryOn: [429, 502, 503],
|
|
337
|
+
maxRetries: 4,
|
|
338
|
+
baseDelayMs: 1e3
|
|
339
|
+
};
|
|
340
|
+
async function withRetry(fn, config) {
|
|
341
|
+
let lastError;
|
|
342
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
343
|
+
try {
|
|
344
|
+
return await fn();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const status = err.status;
|
|
347
|
+
if (status === void 0 || !config.retryOn.includes(status)) {
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
lastError = err;
|
|
351
|
+
if (attempt < config.maxRetries) {
|
|
352
|
+
config.onRetry?.(attempt + 1, status);
|
|
353
|
+
await new Promise(
|
|
354
|
+
(resolve) => setTimeout(resolve, config.baseDelayMs * 2 ** attempt)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
throw lastError;
|
|
360
|
+
}
|
|
361
|
+
|
|
158
362
|
// src/cms.ts
|
|
159
363
|
var DEFAULT_IMAGE_PROXY_BASE = "/api/images";
|
|
160
|
-
function buildListVersion(items) {
|
|
161
|
-
return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
|
|
162
|
-
}
|
|
163
364
|
function resolveDocumentCache(cache) {
|
|
164
365
|
if (!cache || cache.document === false || cache.document === void 0) {
|
|
165
366
|
return noopDocumentCache();
|
|
@@ -183,6 +384,11 @@ var CMS = class {
|
|
|
183
384
|
imageProxyBase;
|
|
184
385
|
contentConfig;
|
|
185
386
|
waitUntil;
|
|
387
|
+
hooks;
|
|
388
|
+
logger;
|
|
389
|
+
retryConfig;
|
|
390
|
+
cached;
|
|
391
|
+
cache;
|
|
186
392
|
constructor(opts) {
|
|
187
393
|
this.source = opts.source;
|
|
188
394
|
this.docCache = resolveDocumentCache(opts.cache);
|
|
@@ -194,23 +400,57 @@ var CMS = class {
|
|
|
194
400
|
this.imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
|
|
195
401
|
this.contentConfig = opts.content;
|
|
196
402
|
this.waitUntil = opts.waitUntil;
|
|
403
|
+
this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks);
|
|
404
|
+
this.logger = mergeLoggers(opts.plugins ?? [], opts.logger);
|
|
405
|
+
this.retryConfig = {
|
|
406
|
+
...DEFAULT_RETRY_CONFIG,
|
|
407
|
+
...opts.rateLimiter
|
|
408
|
+
};
|
|
409
|
+
this.cached = {
|
|
410
|
+
list: this.cachedList.bind(this),
|
|
411
|
+
get: this.cachedGet.bind(this)
|
|
412
|
+
};
|
|
413
|
+
this.cache = {
|
|
414
|
+
prefetchAll: this.prefetchAll.bind(this),
|
|
415
|
+
revalidate: this.revalidate.bind(this),
|
|
416
|
+
sync: this.syncFromWebhook.bind(this),
|
|
417
|
+
checkList: this.checkListUpdate.bind(this),
|
|
418
|
+
checkItem: this.checkItemUpdate.bind(this)
|
|
419
|
+
};
|
|
197
420
|
}
|
|
198
421
|
// ── コンテンツ取得 ──────────────────────────────────────────────────────
|
|
199
422
|
/** 公開済みコンテンツ一覧をソースから直接取得する。 */
|
|
200
423
|
list() {
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
424
|
+
return withRetry(
|
|
425
|
+
() => this.source.list({
|
|
426
|
+
publishedStatuses: this.publishedStatuses.length > 0 ? this.publishedStatuses : void 0
|
|
427
|
+
}),
|
|
428
|
+
{
|
|
429
|
+
...this.retryConfig,
|
|
430
|
+
onRetry: (attempt, status) => {
|
|
431
|
+
this.logger?.warn?.("list() \u30EA\u30C8\u30E9\u30A4\u4E2D", { attempt, status });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
);
|
|
204
435
|
}
|
|
205
436
|
/** スラッグでコンテンツをソースから直接取得する。 */
|
|
206
|
-
async
|
|
207
|
-
const item = await this.source.findBySlug(slug)
|
|
437
|
+
async find(slug) {
|
|
438
|
+
const item = await withRetry(() => this.source.findBySlug(slug), {
|
|
439
|
+
...this.retryConfig,
|
|
440
|
+
onRetry: (attempt, status) => {
|
|
441
|
+
this.logger?.warn?.("find() \u30EA\u30C8\u30E9\u30A4\u4E2D", { attempt, status, slug });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
208
444
|
if (!item) return null;
|
|
209
445
|
if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
|
|
210
446
|
return null;
|
|
211
447
|
}
|
|
212
448
|
return item;
|
|
213
449
|
}
|
|
450
|
+
/** @deprecated find() を使用してください。 */
|
|
451
|
+
findBySlug(slug) {
|
|
452
|
+
return this.find(slug);
|
|
453
|
+
}
|
|
214
454
|
/** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
|
|
215
455
|
isPublished(item) {
|
|
216
456
|
if (this.publishedStatuses.length === 0) return true;
|
|
@@ -220,57 +460,9 @@ var CMS = class {
|
|
|
220
460
|
async render(item) {
|
|
221
461
|
return this.buildCachedItem(item);
|
|
222
462
|
}
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const item = await this.findBySlug(slug);
|
|
227
|
-
if (!item) return null;
|
|
228
|
-
return this.buildCachedItem(item);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
if (isCMSError(err)) throw err;
|
|
231
|
-
throw new CMSError({
|
|
232
|
-
code: "NOTION_FETCH_ITEM_BY_SLUG_FAILED",
|
|
233
|
-
message: "Failed to fetch item by slug from Notion data source.",
|
|
234
|
-
cause: err,
|
|
235
|
-
context: { operation: "renderBySlug", slug }
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
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));
|
|
245
|
-
}
|
|
246
|
-
/** 任意の条件でフィルタリングした一覧を返す。 */
|
|
247
|
-
async where(predicate) {
|
|
248
|
-
const items = await this.list();
|
|
249
|
-
return items.filter(predicate);
|
|
250
|
-
}
|
|
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
|
-
};
|
|
264
|
-
}
|
|
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
|
-
};
|
|
463
|
+
/** QueryBuilder を返す。ステータス・タグ・ページネーションなどを連鎖で指定できる。 */
|
|
464
|
+
query() {
|
|
465
|
+
return new QueryBuilder(this.source, this.publishedStatuses);
|
|
274
466
|
}
|
|
275
467
|
/** 全コンテンツを事前レンダリングしてキャッシュに保存する。 */
|
|
276
468
|
async prefetchAll(opts) {
|
|
@@ -310,7 +502,7 @@ var CMS = class {
|
|
|
310
502
|
async syncFromWebhook(payload) {
|
|
311
503
|
const updated = [];
|
|
312
504
|
if (payload?.slug) {
|
|
313
|
-
const item = await this.
|
|
505
|
+
const item = await this.find(payload.slug);
|
|
314
506
|
if (item) {
|
|
315
507
|
const rendered = await this.buildCachedItem(item);
|
|
316
508
|
await this.docCache.setItem(item.slug, rendered);
|
|
@@ -325,31 +517,36 @@ var CMS = class {
|
|
|
325
517
|
}
|
|
326
518
|
return { updated };
|
|
327
519
|
}
|
|
328
|
-
// ── キャッシュ優先取得(Stale-While-Revalidate)
|
|
520
|
+
// ── キャッシュ優先取得(Stale-While-Revalidate) ──────────────────────
|
|
329
521
|
/** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
|
|
330
|
-
async
|
|
522
|
+
async cachedList() {
|
|
331
523
|
const cached = await this.docCache.getList();
|
|
332
524
|
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
listVersion: buildListVersion(cached.items)
|
|
336
|
-
};
|
|
525
|
+
this.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
|
|
526
|
+
return { items: cached.items, isStale: false, cachedAt: cached.cachedAt };
|
|
337
527
|
}
|
|
528
|
+
this.hooks.onListCacheMiss?.();
|
|
338
529
|
const items = await this.list();
|
|
339
|
-
const
|
|
530
|
+
const cachedAt = Date.now();
|
|
531
|
+
const save = this.docCache.setList({ items, cachedAt });
|
|
340
532
|
if (this.waitUntil) {
|
|
341
533
|
this.waitUntil(save);
|
|
342
534
|
} else {
|
|
343
535
|
await save;
|
|
344
536
|
}
|
|
345
|
-
return { items,
|
|
537
|
+
return { items, isStale: !!cached, cachedAt };
|
|
346
538
|
}
|
|
347
539
|
/** キャッシュ優先で単一コンテンツを返す(SWR)。 */
|
|
348
|
-
async
|
|
540
|
+
async cachedGet(slug) {
|
|
349
541
|
const cached = await this.docCache.getItem(slug);
|
|
350
|
-
if (cached && !isStale(cached.cachedAt, this.ttlMs))
|
|
351
|
-
|
|
352
|
-
|
|
542
|
+
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
543
|
+
this.hooks.onCacheHit?.(slug, cached);
|
|
544
|
+
return cached;
|
|
545
|
+
}
|
|
546
|
+
this.hooks.onCacheMiss?.(slug);
|
|
547
|
+
const item = await this.find(slug);
|
|
548
|
+
if (!item) return null;
|
|
549
|
+
const entry = await this.buildCachedItem(item);
|
|
353
550
|
const save = this.docCache.setItem(slug, entry);
|
|
354
551
|
if (this.waitUntil) {
|
|
355
552
|
this.waitUntil(save);
|
|
@@ -366,12 +563,11 @@ var CMS = class {
|
|
|
366
563
|
return { changed: true, items };
|
|
367
564
|
}
|
|
368
565
|
async checkItemUpdate(slug, lastEdited) {
|
|
369
|
-
const item = await this.
|
|
566
|
+
const item = await this.find(slug);
|
|
370
567
|
if (!item) return { changed: false };
|
|
371
568
|
if (!this.isPublished(item)) return { changed: false };
|
|
372
569
|
if (item.updatedAt === lastEdited) return { changed: false };
|
|
373
|
-
const entry = await this.
|
|
374
|
-
if (!entry) return { changed: false };
|
|
570
|
+
const entry = await this.buildCachedItem(item);
|
|
375
571
|
await this.docCache.setItem(slug, entry);
|
|
376
572
|
return {
|
|
377
573
|
changed: true,
|
|
@@ -380,6 +576,45 @@ var CMS = class {
|
|
|
380
576
|
notionUpdatedAt: entry.notionUpdatedAt
|
|
381
577
|
};
|
|
382
578
|
}
|
|
579
|
+
// ── 後方互換 SWR ────────────────────────────────────────────────────────
|
|
580
|
+
/** @deprecated cached.list() を使用してください。 */
|
|
581
|
+
async getList() {
|
|
582
|
+
const result = await this.cachedList();
|
|
583
|
+
return { items: result.items, listVersion: buildListVersion(result.items) };
|
|
584
|
+
}
|
|
585
|
+
/** @deprecated cached.get() を使用してください。 */
|
|
586
|
+
getItem(slug) {
|
|
587
|
+
return this.cachedGet(slug);
|
|
588
|
+
}
|
|
589
|
+
/** @deprecated cache.prefetchAll() を使用してください。 */
|
|
590
|
+
async prefetchAllLegacy(opts) {
|
|
591
|
+
return this.prefetchAll(opts);
|
|
592
|
+
}
|
|
593
|
+
// ── 後方互換クエリ API ──────────────────────────────────────────────────
|
|
594
|
+
/** @deprecated query().status(s).execute() を使用してください。 */
|
|
595
|
+
async listByStatus(status) {
|
|
596
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
597
|
+
return this.query().status(statuses).execute().then((r) => r.items);
|
|
598
|
+
}
|
|
599
|
+
/** @deprecated query().where(pred).execute() を使用してください。 */
|
|
600
|
+
async where(predicate) {
|
|
601
|
+
return this.query().where(predicate).execute().then((r) => r.items);
|
|
602
|
+
}
|
|
603
|
+
/** @deprecated query().paginate(opts).execute() を使用してください。 */
|
|
604
|
+
async paginate(opts) {
|
|
605
|
+
const result = await this.query().paginate(opts).execute();
|
|
606
|
+
return {
|
|
607
|
+
items: result.items,
|
|
608
|
+
total: result.total,
|
|
609
|
+
page: result.page,
|
|
610
|
+
perPage: result.perPage,
|
|
611
|
+
hasNext: result.hasNext
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/** @deprecated query().adjacent(slug) を使用してください。 */
|
|
615
|
+
getAdjacent(slug) {
|
|
616
|
+
return this.query().adjacent(slug);
|
|
617
|
+
}
|
|
383
618
|
// ── 画像配信 ───────────────────────────────────────────────────────────
|
|
384
619
|
/** ハッシュキーでキャッシュ画像を取得する。 */
|
|
385
620
|
getCachedImage(hash) {
|
|
@@ -396,6 +631,11 @@ var CMS = class {
|
|
|
396
631
|
}
|
|
397
632
|
// ── プライベートヘルパー ────────────────────────────────────────────────
|
|
398
633
|
async buildCachedItem(item) {
|
|
634
|
+
const start = Date.now();
|
|
635
|
+
this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u958B\u59CB", {
|
|
636
|
+
slug: item.slug,
|
|
637
|
+
pageId: item.id
|
|
638
|
+
});
|
|
399
639
|
let markdown;
|
|
400
640
|
try {
|
|
401
641
|
markdown = await this.source.loadMarkdown(item);
|
|
@@ -435,14 +675,28 @@ var CMS = class {
|
|
|
435
675
|
}
|
|
436
676
|
});
|
|
437
677
|
}
|
|
438
|
-
|
|
678
|
+
if (this.hooks.afterRender) {
|
|
679
|
+
html = await this.hooks.afterRender(html, item);
|
|
680
|
+
}
|
|
681
|
+
let result = {
|
|
439
682
|
html,
|
|
440
683
|
item,
|
|
441
684
|
notionUpdatedAt: item.updatedAt,
|
|
442
685
|
cachedAt: Date.now()
|
|
443
686
|
};
|
|
687
|
+
if (this.hooks.beforeCache) {
|
|
688
|
+
result = await this.hooks.beforeCache(result);
|
|
689
|
+
}
|
|
690
|
+
this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u5B8C\u4E86", {
|
|
691
|
+
slug: item.slug,
|
|
692
|
+
durationMs: Date.now() - start
|
|
693
|
+
});
|
|
694
|
+
return result;
|
|
444
695
|
}
|
|
445
696
|
};
|
|
697
|
+
function buildListVersion(items) {
|
|
698
|
+
return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
|
|
699
|
+
}
|
|
446
700
|
function createCMS(opts) {
|
|
447
701
|
return new CMS(opts);
|
|
448
702
|
}
|
|
@@ -484,10 +738,18 @@ function mapItem(page, props) {
|
|
|
484
738
|
}
|
|
485
739
|
return parsed.data;
|
|
486
740
|
}
|
|
741
|
+
|
|
742
|
+
// src/types/plugin.ts
|
|
743
|
+
function definePlugin(plugin) {
|
|
744
|
+
return plugin;
|
|
745
|
+
}
|
|
487
746
|
export {
|
|
488
747
|
CMS,
|
|
489
748
|
CMSError,
|
|
749
|
+
DEFAULT_RETRY_CONFIG,
|
|
750
|
+
QueryBuilder,
|
|
490
751
|
createCMS,
|
|
752
|
+
definePlugin,
|
|
491
753
|
getPlainText,
|
|
492
754
|
isCMSError,
|
|
493
755
|
isStale,
|
|
@@ -495,7 +757,10 @@ export {
|
|
|
495
757
|
memoryCache,
|
|
496
758
|
memoryDocumentCache,
|
|
497
759
|
memoryImageCache,
|
|
760
|
+
mergeHooks,
|
|
761
|
+
mergeLoggers,
|
|
498
762
|
noopDocumentCache,
|
|
499
763
|
noopImageCache,
|
|
500
|
-
sha256Hex
|
|
764
|
+
sha256Hex,
|
|
765
|
+
withRetry
|
|
501
766
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notion-headless-cms/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Core CMS engine for notion-headless-cms — fetch, transform, cache with stale-while-revalidate strategy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"notion",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"@notionhq/client": "^5.18.0",
|
|
43
43
|
"unified": "^11.0.5",
|
|
44
44
|
"zod": "^4.1.12",
|
|
45
|
-
"@notion-headless-cms/fetcher": "0.0
|
|
46
|
-
"@notion-headless-cms/transformer": "0.
|
|
47
|
-
"@notion-headless-cms/renderer": "0.0
|
|
45
|
+
"@notion-headless-cms/fetcher": "0.1.0",
|
|
46
|
+
"@notion-headless-cms/transformer": "0.1.1",
|
|
47
|
+
"@notion-headless-cms/renderer": "0.1.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsup src/index.ts --format esm --dts --out-dir dist",
|