@notion-headless-cms/core 0.3.8 → 0.3.10

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.mjs CHANGED
@@ -1,5 +1,4 @@
1
- import { memoryDocumentCache, memoryImageCache } from "./cache/memory.mjs";
2
- import { noopDocumentCache, noopImageCache } from "./cache/noop.mjs";
1
+ import { memoryCache } from "./cache/memory.mjs";
3
2
  import { CMSError, isCMSError, isCMSErrorInNamespace } from "./errors.mjs";
4
3
  import { mergeHooks, mergeLoggers } from "./hooks.mjs";
5
4
  //#region src/cache.ts
@@ -18,6 +17,47 @@ function isStale(cachedAt, ttlMs) {
18
17
  return Date.now() - cachedAt > ttlMs;
19
18
  }
20
19
  //#endregion
20
+ //#region src/cache/noop.ts
21
+ /** 何もキャッシュしないドキュメントオペレーション。常に null を返す。 */
22
+ const noopDoc = {
23
+ getList(_collection) {
24
+ return Promise.resolve(null);
25
+ },
26
+ setList(_collection, _data) {
27
+ return Promise.resolve();
28
+ },
29
+ getMeta(_collection, _slug) {
30
+ return Promise.resolve(null);
31
+ },
32
+ setMeta(_collection, _slug, _data) {
33
+ return Promise.resolve();
34
+ },
35
+ getContent(_collection, _slug) {
36
+ return Promise.resolve(null);
37
+ },
38
+ setContent(_collection, _slug, _data) {
39
+ return Promise.resolve();
40
+ },
41
+ invalidate() {
42
+ return Promise.resolve();
43
+ }
44
+ };
45
+ /** 何もキャッシュしない画像オペレーション。 */
46
+ const noopImg = {
47
+ get(_hash) {
48
+ return Promise.resolve(null);
49
+ },
50
+ set() {
51
+ return Promise.resolve();
52
+ }
53
+ };
54
+ /**
55
+ * 何もキャッシュしないアダプタ。`createCMS({ cache })` 未指定時の内部デフォルト。
56
+ * テストでも使える。
57
+ */
58
+ const noopDocOps = noopDoc;
59
+ const noopImgOps = noopImg;
60
+ //#endregion
21
61
  //#region src/image.ts
22
62
  /** レスポンスヘッダまたはURLの拡張子からContent-Typeを推測する。 */
23
63
  function inferContentType(url, responseContentType) {
@@ -28,23 +68,47 @@ function inferContentType(url, responseContentType) {
28
68
  return "image/jpeg";
29
69
  }
30
70
  /**
31
- * Notion画像URLをfetchしてImageCacheAdapterにキャッシュし、プロキシURL を返す。
71
+ * URL → SHA-256 hash のメモ化マップ。
72
+ * Notion の画像 URL は同じ画像でも署名が時刻ごとに変わるが、
73
+ * 1 リクエスト内では同一 URL が複数回現れることが多い (重複ハッシュ計算を回避)。
74
+ *
75
+ * メモリリーク防止に最大エントリ数を設けており、超過時は最古から削除する LRU。
76
+ */
77
+ const HASH_MEMO_LIMIT = 1024;
78
+ const hashMemo = /* @__PURE__ */ new Map();
79
+ async function memoSha256(url) {
80
+ const cached = hashMemo.get(url);
81
+ if (cached !== void 0) {
82
+ hashMemo.delete(url);
83
+ hashMemo.set(url, cached);
84
+ return cached;
85
+ }
86
+ const hash = await sha256Hex(url);
87
+ hashMemo.set(url, hash);
88
+ if (hashMemo.size > HASH_MEMO_LIMIT) {
89
+ const firstKey = hashMemo.keys().next().value;
90
+ if (firstKey !== void 0) hashMemo.delete(firstKey);
91
+ }
92
+ return hash;
93
+ }
94
+ /**
95
+ * Notion画像URLをfetchして ImageCacheOps にキャッシュし、プロキシURL を返す。
32
96
  * 既存キャッシュがあれば再fetchしない。
33
97
  */
34
- async function fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger) {
35
- const hash = await sha256Hex(notionUrl);
98
+ async function fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, logger) {
99
+ const hash = await memoSha256(notionUrl);
36
100
  const proxyUrl = `${imageProxyBase}/${hash}`;
37
101
  if (await cache.get(hash)) {
38
102
  logger?.debug?.("画像キャッシュヒット", {
39
103
  operation: "fetchAndCacheImage",
40
- cacheAdapter: cache.name,
104
+ cacheAdapter: cacheName,
41
105
  imageHash: hash
42
106
  });
43
107
  return proxyUrl;
44
108
  }
45
109
  logger?.debug?.("画像キャッシュミス、Notion からフェッチ", {
46
110
  operation: "fetchAndCacheImage",
47
- cacheAdapter: cache.name,
111
+ cacheAdapter: cacheName,
48
112
  imageHash: hash
49
113
  });
50
114
  try {
@@ -63,7 +127,7 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger) {
63
127
  await cache.set(hash, data, contentType);
64
128
  logger?.debug?.("画像をキャッシュに保存", {
65
129
  operation: "fetchAndCacheImage",
66
- cacheAdapter: cache.name,
130
+ cacheAdapter: cacheName,
67
131
  imageHash: hash
68
132
  });
69
133
  } catch (err) {
@@ -81,15 +145,12 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger) {
81
145
  return proxyUrl;
82
146
  }
83
147
  /**
84
- * ImageCacheAdapter と imageProxyBase から cacheImage 関数を構築するファクトリ。
85
- *
86
- * 返り値の関数は Notion 画像 URL を受け取り、SHA-256 ハッシュをキャッシュキーとして
87
- * {@link ImageCacheAdapter} に保存後、フロントエンドへの配信用プロキシ URL を返す。
88
- * Content-Type はレスポンスヘッダまたは URL の拡張子から推測する。
89
- * タイムアウトは 10 秒固定。
148
+ * `ImageCacheOps``imageProxyBase` から `cacheImage` 関数を構築する。
149
+ * 返り値は Notion 画像 URL を受け取り、SHA-256 ハッシュをキャッシュキーとして
150
+ * {@link ImageCacheOps} に保存後、プロキシ URL を返す。
90
151
  */
91
- function buildCacheImageFn(cache, imageProxyBase, logger) {
92
- return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase, logger);
152
+ function buildCacheImageFn(cache, cacheName, imageProxyBase, logger) {
153
+ return (notionUrl) => fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, logger);
93
154
  }
94
155
  //#endregion
95
156
  //#region src/rendering.ts
@@ -140,7 +201,7 @@ async function buildCachedItemContent(item, ctx) {
140
201
  });
141
202
  blocks = [];
142
203
  }
143
- const cacheImage = ctx.hasImageCache ? buildCacheImageFn(ctx.imgCache, ctx.imageProxyBase, ctx.logger) : void 0;
204
+ const cacheImage = ctx.hasImageCache ? buildCacheImageFn(ctx.imgCache, ctx.imgCacheName, ctx.imageProxyBase, ctx.logger) : void 0;
144
205
  let html;
145
206
  const rendererFn = ctx.rendererFn ?? await loadDefaultRenderer();
146
207
  try {
@@ -239,24 +300,41 @@ async function withRetry(fn, config) {
239
300
  /**
240
301
  * コレクション別キャッシュキーを生成する。
241
302
  * item: `{collection}:{slug}` / list: `{collection}`
303
+ *
304
+ * (Cache adapter 内部のキー戦略はアダプタごとに異なるが、
305
+ * 表示や再計算用に core 側でも公開ヘルパーを提供する)
242
306
  */
243
307
  function collectionKey(collection, slug) {
244
308
  return slug ? `${collection}:${slug}` : collection;
245
309
  }
246
310
  /** CollectionClient の実装。ユーザーは `createCMS` 経由でインスタンスを受け取る。 */
247
311
  var CollectionClientImpl = class {
312
+ cache;
248
313
  constructor(ctx) {
249
314
  this.ctx = ctx;
315
+ this.cache = {
316
+ invalidate: (slug) => this.invalidateImpl(slug),
317
+ warm: (opts) => this.warmImpl(opts),
318
+ adjacent: (slug, opts) => this.adjacentImpl(slug, opts)
319
+ };
250
320
  }
251
- async getItem(slug) {
252
- const cachedMeta = await this.ctx.docCache.getItemMeta(slug);
321
+ async get(slug, opts = {}) {
322
+ if (opts.fresh) {
323
+ this.ctx.hooks.onCacheMiss?.(slug);
324
+ const item = await this.findRaw(slug);
325
+ if (!item) return null;
326
+ const meta = await this.persistMeta(slug, item);
327
+ await this.invalidateContent(slug);
328
+ return this.attachLazyContent(meta);
329
+ }
330
+ const cachedMeta = await this.ctx.docCache.getMeta(this.ctx.collection, slug);
253
331
  if (cachedMeta) {
254
332
  if (this.ctx.ttlMs !== void 0 && isStale(cachedMeta.cachedAt, this.ctx.ttlMs)) {
255
333
  this.ctx.logger?.debug?.("キャッシュ期限切れ(TTL)、フェッチ", {
256
- operation: "getItem",
334
+ operation: "get",
257
335
  slug,
258
336
  collection: this.ctx.collection,
259
- cacheAdapter: this.ctx.docCache.name
337
+ cacheAdapter: this.ctx.docCacheName
260
338
  });
261
339
  this.ctx.hooks.onCacheMiss?.(slug);
262
340
  const item = await this.findRaw(slug);
@@ -268,154 +346,55 @@ var CollectionClientImpl = class {
268
346
  const bg = this.checkAndUpdateItemBg(slug, cachedMeta);
269
347
  if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
270
348
  this.ctx.logger?.debug?.("キャッシュヒット", {
271
- operation: "getItem",
349
+ operation: "get",
272
350
  slug,
273
351
  collection: this.ctx.collection,
274
- cacheAdapter: this.ctx.docCache.name,
352
+ cacheAdapter: this.ctx.docCacheName,
275
353
  cachedAt: cachedMeta.cachedAt
276
354
  });
277
355
  this.ctx.hooks.onCacheHit?.(slug, cachedMeta);
278
356
  return this.attachLazyContent(cachedMeta);
279
357
  }
280
358
  this.ctx.logger?.debug?.("キャッシュミス、フェッチ", {
281
- operation: "getItem",
359
+ operation: "get",
282
360
  slug,
283
361
  collection: this.ctx.collection,
284
- cacheAdapter: this.ctx.docCache.name
362
+ cacheAdapter: this.ctx.docCacheName
285
363
  });
286
364
  this.ctx.hooks.onCacheMiss?.(slug);
287
365
  const item = await this.findRaw(slug);
288
- if (!item) {
289
- this.ctx.logger?.debug?.("アイテムが見つかりません", {
290
- operation: "getItem",
291
- slug,
292
- collection: this.ctx.collection
293
- });
294
- return null;
295
- }
366
+ if (!item) return null;
296
367
  const meta = await this.persistMeta(slug, item, { background: true });
297
368
  return this.attachLazyContent(meta);
298
369
  }
299
- async getItemMeta(slug) {
300
- const cachedMeta = await this.ctx.docCache.getItemMeta(slug);
301
- if (cachedMeta) {
302
- if (this.ctx.ttlMs !== void 0 && isStale(cachedMeta.cachedAt, this.ctx.ttlMs)) {
303
- const item = await this.findRaw(slug);
304
- if (!item) return null;
305
- const meta = await this.persistMeta(slug, item);
306
- await this.invalidateContent(slug);
307
- return meta.item;
308
- }
309
- const bg = this.checkAndUpdateItemBg(slug, cachedMeta);
310
- if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
311
- this.ctx.hooks.onCacheHit?.(slug, cachedMeta);
312
- return cachedMeta.item;
313
- }
314
- this.ctx.hooks.onCacheMiss?.(slug);
315
- const item = await this.findRaw(slug);
316
- if (!item) return null;
317
- return (await this.persistMeta(slug, item, { background: true })).item;
318
- }
319
- async getItemContent(slug) {
320
- const meta = await this.ctx.docCache.getItemMeta(slug);
321
- const item = meta?.item ?? await this.findRaw(slug);
322
- if (!item) return null;
323
- if (!meta) await this.persistMeta(slug, item);
324
- return toContentPayload(await this.loadOrBuildContent(slug, item));
325
- }
326
- async getList(opts) {
327
- const items = applyGetListOptions(await this.fetchList(), opts);
328
- return {
329
- items,
330
- version: this.ctx.source.getListVersion(items)
331
- };
370
+ async list(opts) {
371
+ return applyListOptions(await this.fetchList(), opts);
332
372
  }
333
- async getStaticParams() {
373
+ async params() {
334
374
  return (await this.fetchList()).map((item) => ({ slug: item.slug }));
335
375
  }
336
- async getStaticPaths() {
337
- return (await this.fetchList()).map((item) => item.slug);
338
- }
339
- async adjacent(slug, opts) {
340
- const items = applyGetListOptions(await this.fetchList(), { sort: opts?.sort });
341
- const index = items.findIndex((it) => it.slug === slug);
342
- if (index === -1) return {
343
- prev: null,
344
- next: null
345
- };
346
- return {
347
- prev: index > 0 ? items[index - 1] ?? null : null,
348
- next: index < items.length - 1 ? items[index + 1] ?? null : null
349
- };
350
- }
351
- async revalidate(slug) {
352
- this.ctx.logger?.debug?.("アイテムキャッシュを無効化", {
353
- operation: "revalidate",
354
- collection: this.ctx.collection,
355
- cacheAdapter: this.ctx.docCache.name,
356
- slug
357
- });
358
- if (!this.ctx.docCache.invalidate) return;
359
- await this.ctx.docCache.invalidate({
360
- collection: this.ctx.collection,
361
- slug
362
- });
363
- }
364
- async revalidateAll() {
365
- this.ctx.logger?.debug?.("コレクション全体のキャッシュを無効化", {
366
- operation: "revalidateAll",
367
- collection: this.ctx.collection,
368
- cacheAdapter: this.ctx.docCache.name
369
- });
370
- if (!this.ctx.docCache.invalidate) return;
371
- await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
372
- }
373
- async checkForUpdate({ slug, since }) {
374
- const fresh = await this.findRaw(slug);
375
- if (!fresh) return { changed: false };
376
- const lm = this.ctx.source.getLastModified(fresh);
377
- if (lm === since) {
378
- this.ctx.logger?.debug?.("checkForUpdate: 差分なし", {
379
- operation: "checkForUpdate",
380
- slug,
376
+ async invalidateImpl(slug) {
377
+ if (slug !== void 0) {
378
+ this.ctx.logger?.debug?.("アイテムキャッシュを無効化", {
379
+ operation: "cache.invalidate",
380
+ collection: this.ctx.collection,
381
+ cacheAdapter: this.ctx.docCacheName,
382
+ slug
383
+ });
384
+ await this.ctx.docCache.invalidate({
381
385
  collection: this.ctx.collection,
382
- since
386
+ slug
383
387
  });
384
- return { changed: false };
388
+ return;
385
389
  }
386
- const meta = await this.persistMeta(slug, fresh);
387
- await this.invalidateContent(slug);
388
- this.ctx.hooks.onCacheRevalidated?.(slug, meta);
389
- this.ctx.logger?.debug?.("checkForUpdate: 差分を検出", {
390
- operation: "checkForUpdate",
391
- slug,
390
+ this.ctx.logger?.debug?.("コレクション全体のキャッシュを無効化", {
391
+ operation: "cache.invalidate",
392
392
  collection: this.ctx.collection,
393
- since,
394
- notionUpdatedAt: lm
395
- });
396
- const bg = this.rebuildContentBg(slug, fresh);
397
- if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
398
- return {
399
- changed: true,
400
- meta: fresh
401
- };
402
- }
403
- async checkListForUpdate({ since, filter }) {
404
- const allItems = await this.fetchListRaw();
405
- const items = applyGetListOptions(allItems, filter);
406
- const version = this.ctx.source.getListVersion(items);
407
- if (version === since) return { changed: false };
408
- await this.ctx.docCache.setList({
409
- items: allItems,
410
- cachedAt: Date.now()
393
+ cacheAdapter: this.ctx.docCacheName
411
394
  });
412
- return {
413
- changed: true,
414
- items,
415
- version
416
- };
395
+ await this.ctx.docCache.invalidate({ collection: this.ctx.collection });
417
396
  }
418
- async prefetch(opts) {
397
+ async warmImpl(opts) {
419
398
  const items = await this.fetchListRaw();
420
399
  const concurrency = opts?.concurrency ?? this.ctx.maxConcurrent;
421
400
  let ok = 0;
@@ -426,11 +405,11 @@ var CollectionClientImpl = class {
426
405
  try {
427
406
  await this.persistMeta(item.slug, item);
428
407
  const content = await buildCachedItemContent(item, this.ctx.render);
429
- await this.ctx.docCache.setItemContent(item.slug, content);
408
+ await this.ctx.docCache.setContent(this.ctx.collection, item.slug, content);
430
409
  ok++;
431
410
  } catch (err) {
432
411
  failed++;
433
- this.ctx.logger?.warn?.("prefetch: アイテムの事前レンダリングに失敗", {
412
+ this.ctx.logger?.warn?.("warm: アイテムの事前レンダリングに失敗", {
434
413
  slug: item.slug,
435
414
  pageId: item.id,
436
415
  error: err instanceof Error ? err.message : String(err)
@@ -439,7 +418,7 @@ var CollectionClientImpl = class {
439
418
  }));
440
419
  opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);
441
420
  }
442
- await this.ctx.docCache.setList({
421
+ await this.ctx.docCache.setList(this.ctx.collection, {
443
422
  items,
444
423
  cachedAt: Date.now()
445
424
  });
@@ -448,16 +427,27 @@ var CollectionClientImpl = class {
448
427
  failed
449
428
  };
450
429
  }
430
+ async adjacentImpl(slug, opts) {
431
+ const items = applyListOptions(await this.fetchList(), { sort: opts?.sort });
432
+ const index = items.findIndex((it) => it.slug === slug);
433
+ if (index === -1) return {
434
+ prev: null,
435
+ next: null
436
+ };
437
+ return {
438
+ prev: index > 0 ? items[index - 1] ?? null : null,
439
+ next: index < items.length - 1 ? items[index + 1] ?? null : null
440
+ };
441
+ }
451
442
  async persistMeta(slug, item, opts = {}) {
452
443
  let meta = buildCachedItemMeta(item, this.ctx.source);
453
444
  if (this.ctx.hooks.beforeCacheMeta) meta = await this.ctx.hooks.beforeCacheMeta(meta);
454
- const save = this.ctx.docCache.setItemMeta(slug, meta);
445
+ const save = this.ctx.docCache.setMeta(this.ctx.collection, slug, meta);
455
446
  if (opts.background && this.ctx.waitUntil) this.ctx.waitUntil(save);
456
447
  else await save;
457
448
  return meta;
458
449
  }
459
450
  async invalidateContent(slug) {
460
- if (!this.ctx.docCache.invalidate) return;
461
451
  await this.ctx.docCache.invalidate({
462
452
  collection: this.ctx.collection,
463
453
  slug,
@@ -470,21 +460,18 @@ var CollectionClientImpl = class {
470
460
  */
471
461
  async loadOrBuildContent(slug, item) {
472
462
  const expected = this.ctx.source.getLastModified(item);
473
- const cached = await this.ctx.docCache.getItemContent(slug);
463
+ const cached = await this.ctx.docCache.getContent(this.ctx.collection, slug);
474
464
  if (cached && cached.notionUpdatedAt === expected) return cached;
475
465
  const fresh = await buildCachedItemContent(item, this.ctx.render);
476
- await this.ctx.docCache.setItemContent(slug, fresh);
466
+ await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);
477
467
  this.ctx.hooks.onContentRevalidated?.(slug, fresh);
478
468
  return fresh;
479
469
  }
480
- /**
481
- * メタは既知(差分検出済み or 直前にフェッチ済み)の状態で本文だけ
482
- * バックグラウンド再生成する。エラーは握りつぶしてログのみ。
483
- */
470
+ /** メタ既知の状態で本文だけバックグラウンド再生成する。エラーは握りつぶす。 */
484
471
  async rebuildContentBg(slug, item) {
485
472
  try {
486
473
  const fresh = await buildCachedItemContent(item, this.ctx.render);
487
- await this.ctx.docCache.setItemContent(slug, fresh);
474
+ await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);
488
475
  this.ctx.hooks.onContentRevalidated?.(slug, fresh);
489
476
  } catch (err) {
490
477
  this.ctx.logger?.warn?.("本文のバックグラウンド再生成に失敗", {
@@ -502,30 +489,24 @@ var CollectionClientImpl = class {
502
489
  if (!payloadPromise) payloadPromise = this.loadOrBuildContent(slug, item);
503
490
  return payloadPromise;
504
491
  };
505
- return Object.assign(Object.create(null), item, { content: {
506
- async blocks() {
507
- return (await loadPayload()).blocks;
508
- },
509
- async html() {
510
- return (await loadPayload()).html;
511
- },
512
- async markdown() {
513
- return (await loadPayload()).markdown;
514
- }
515
- } });
492
+ const render = async (opts) => {
493
+ const payload = await loadPayload();
494
+ return opts?.format === "markdown" ? payload.markdown : payload.html;
495
+ };
496
+ return Object.assign(Object.create(null), item, { render });
516
497
  }
517
498
  async fetchList() {
518
- const cached = await this.ctx.docCache.getList();
499
+ const cached = await this.ctx.docCache.getList(this.ctx.collection);
519
500
  if (cached) {
520
501
  if (this.ctx.ttlMs !== void 0 && isStale(cached.cachedAt, this.ctx.ttlMs)) {
521
502
  this.ctx.logger?.debug?.("リストキャッシュ期限切れ(TTL)、フェッチ", {
522
- operation: "getList",
503
+ operation: "list",
523
504
  collection: this.ctx.collection,
524
- cacheAdapter: this.ctx.docCache.name
505
+ cacheAdapter: this.ctx.docCacheName
525
506
  });
526
507
  this.ctx.hooks.onListCacheMiss?.();
527
508
  const items = await this.fetchListRaw();
528
- await this.ctx.docCache.setList({
509
+ await this.ctx.docCache.setList(this.ctx.collection, {
529
510
  items,
530
511
  cachedAt: Date.now()
531
512
  });
@@ -534,22 +515,22 @@ var CollectionClientImpl = class {
534
515
  const bg = this.checkAndUpdateListBg(cached);
535
516
  if (this.ctx.waitUntil) this.ctx.waitUntil(bg);
536
517
  this.ctx.logger?.debug?.("リストキャッシュヒット", {
537
- operation: "getList",
518
+ operation: "list",
538
519
  collection: this.ctx.collection,
539
- cacheAdapter: this.ctx.docCache.name
520
+ cacheAdapter: this.ctx.docCacheName
540
521
  });
541
522
  this.ctx.hooks.onListCacheHit?.(cached);
542
523
  return cached.items;
543
524
  }
544
525
  this.ctx.logger?.debug?.("リストキャッシュミス、フェッチ", {
545
- operation: "getList",
526
+ operation: "list",
546
527
  collection: this.ctx.collection,
547
- cacheAdapter: this.ctx.docCache.name
528
+ cacheAdapter: this.ctx.docCacheName
548
529
  });
549
530
  this.ctx.hooks.onListCacheMiss?.();
550
531
  const items = await this.fetchListRaw();
551
532
  const cachedAt = Date.now();
552
- const save = this.ctx.docCache.setList({
533
+ const save = this.ctx.docCache.setList(this.ctx.collection, {
553
534
  items,
554
535
  cachedAt
555
536
  });
@@ -565,7 +546,7 @@ var CollectionClientImpl = class {
565
546
  const meta = await this.persistMeta(slug, item);
566
547
  await this.invalidateContent(slug);
567
548
  this.ctx.logger?.debug?.("SWR: 差分を検出、メタを差し替え", {
568
- operation: "getItem:bg",
549
+ operation: "get:bg",
569
550
  slug,
570
551
  collection: this.ctx.collection,
571
552
  notionUpdatedAt: cached.notionUpdatedAt
@@ -573,12 +554,12 @@ var CollectionClientImpl = class {
573
554
  this.ctx.hooks.onCacheRevalidated?.(slug, meta);
574
555
  await this.rebuildContentBg(slug, item);
575
556
  } else if (this.ctx.ttlMs !== void 0) {
576
- await this.ctx.docCache.setItemMeta(slug, {
557
+ await this.ctx.docCache.setMeta(this.ctx.collection, slug, {
577
558
  ...cached,
578
559
  cachedAt: Date.now()
579
560
  });
580
561
  this.ctx.logger?.debug?.("SWR: 差分なし、TTL をリセット", {
581
- operation: "getItem:bg",
562
+ operation: "get:bg",
582
563
  slug,
583
564
  collection: this.ctx.collection
584
565
  });
@@ -599,19 +580,19 @@ var CollectionClientImpl = class {
599
580
  items,
600
581
  cachedAt: Date.now()
601
582
  };
602
- await this.ctx.docCache.setList(listEntry);
583
+ await this.ctx.docCache.setList(this.ctx.collection, listEntry);
603
584
  this.ctx.logger?.debug?.("SWR: リスト差分を検出、キャッシュを差し替え", {
604
- operation: "getList:bg",
585
+ operation: "list:bg",
605
586
  collection: this.ctx.collection
606
587
  });
607
588
  this.ctx.hooks.onListCacheRevalidated?.(listEntry);
608
589
  } else if (this.ctx.ttlMs !== void 0) {
609
- await this.ctx.docCache.setList({
590
+ await this.ctx.docCache.setList(this.ctx.collection, {
610
591
  ...cached,
611
592
  cachedAt: Date.now()
612
593
  });
613
594
  this.ctx.logger?.debug?.("SWR: リスト差分なし、TTL をリセット", {
614
- operation: "getList:bg",
595
+ operation: "list:bg",
615
596
  collection: this.ctx.collection
616
597
  });
617
598
  }
@@ -626,7 +607,7 @@ var CollectionClientImpl = class {
626
607
  return withRetry(() => this.ctx.source.list({ publishedStatuses: this.ctx.publishedStatuses.length > 0 ? this.ctx.publishedStatuses : void 0 }), {
627
608
  ...this.ctx.retryConfig,
628
609
  onRetry: (attempt, status) => {
629
- this.ctx.logger?.warn?.("getList() リトライ中", {
610
+ this.ctx.logger?.warn?.("list() リトライ中", {
630
611
  attempt,
631
612
  status
632
613
  });
@@ -637,15 +618,14 @@ var CollectionClientImpl = class {
637
618
  const retryOpts = {
638
619
  ...this.ctx.retryConfig,
639
620
  onRetry: (attempt, status) => {
640
- this.ctx.logger?.warn?.("getItem() リトライ中", {
621
+ this.ctx.logger?.warn?.("get() リトライ中", {
641
622
  attempt,
642
623
  status,
643
624
  slug
644
625
  });
645
626
  }
646
627
  };
647
- const slugField = this.ctx.slugField;
648
- const notionPropName = slugField ? this.ctx.source.properties?.[slugField]?.notion : void 0;
628
+ const notionPropName = this.ctx.source.properties?.[this.ctx.slugField]?.notion;
649
629
  let item;
650
630
  const findByProp = this.ctx.source.findByProp?.bind(this.ctx.source);
651
631
  if (notionPropName && findByProp) item = await withRetry(() => findByProp(notionPropName, slug), retryOpts);
@@ -655,20 +635,12 @@ var CollectionClientImpl = class {
655
635
  return item;
656
636
  }
657
637
  };
658
- function toContentPayload(c) {
659
- return {
660
- html: c.html,
661
- markdown: c.markdown,
662
- blocks: c.blocks,
663
- notionUpdatedAt: c.notionUpdatedAt
664
- };
665
- }
666
- function applyGetListOptions(items, opts) {
638
+ function applyListOptions(items, opts) {
667
639
  if (!opts) return items;
668
640
  let result = items;
669
- if (opts.statuses && opts.statuses.length > 0) {
670
- const allow = new Set(opts.statuses);
671
- result = result.filter((it) => it.status !== void 0 && allow.has(it.status));
641
+ if (opts.status) {
642
+ const allow = new Set(Array.isArray(opts.status) ? opts.status : [opts.status]);
643
+ result = result.filter((it) => it.status != null && allow.has(it.status));
672
644
  }
673
645
  if (opts.tag) {
674
646
  const tag = opts.tag;
@@ -689,7 +661,7 @@ function applyGetListOptions(items, opts) {
689
661
  }
690
662
  function makeComparator(sort) {
691
663
  const by = sort.by;
692
- const dir = sort.direction === "asc" ? 1 : -1;
664
+ const dir = sort.dir === "asc" ? 1 : -1;
693
665
  return (a, b) => {
694
666
  const av = a[by];
695
667
  const bv = b[by];
@@ -757,97 +729,41 @@ function trimTrailingSlash(s) {
757
729
  return s.endsWith("/") ? s.slice(0, -1) : s;
758
730
  }
759
731
  //#endregion
760
- //#region src/preset-node.ts
761
- /**
762
- * Node.js ランタイム向けの `createCMS` オプションプリセット。
763
- * メモリキャッシュをデフォルト有効にした `{ cache, renderer }` を返す。
764
- *
765
- * @example
766
- * import { createCMS, nodePreset } from "@notion-headless-cms/core";
767
- * import { cmsDataSources } from "./generated/cms-schema";
768
- *
769
- * const cms = createCMS({
770
- * ...nodePreset({ ttlMs: 5 * 60_000 }),
771
- * dataSources: cmsDataSources,
772
- * });
773
- */
774
- function nodePreset(opts = {}) {
775
- if (opts.cache === "disabled") return {
776
- cache: void 0,
777
- renderer: opts.renderer
778
- };
779
- return {
780
- cache: opts.cache ?? {
781
- document: memoryDocumentCache(),
782
- image: memoryImageCache(),
783
- ttlMs: opts.ttlMs
784
- },
785
- renderer: opts.renderer
786
- };
787
- }
788
- //#endregion
789
732
  //#region src/cms.ts
790
733
  const DEFAULT_IMAGE_PROXY_BASE = "/api/images";
791
- function resolveDocumentCache(cache) {
792
- if (!cache || cache === "disabled" || !cache.document) return noopDocumentCache();
793
- return cache.document;
794
- }
795
- function resolveImageCache(cache) {
796
- if (!cache || cache === "disabled" || !cache.image) return noopImageCache();
797
- return cache.image;
798
- }
799
- function resolveTtl(cache) {
800
- if (!cache || cache === "disabled") return void 0;
801
- return cache.ttlMs;
802
- }
803
- function hasImageCacheConfigured(cache) {
804
- if (!cache || cache === "disabled") return false;
805
- return !!cache.image;
806
- }
807
734
  /**
808
- * `{collection}:{slug}` キー空間で動作するコレクション別キャッシュビューを生成する。
809
- * 単一の `DocumentCacheAdapter` に複数コレクションを同居させるためのアダプタ。
735
+ * `cache` オプションから document / image オペレーションを解決する。
736
+ *
737
+ * - 配列で渡された場合は各 adapter の `handles` を見て先勝ち (最初に見つかったもの) で振り分ける
738
+ * - 単体で渡された場合は `handles` の領域だけ反映、片側は noop
739
+ * - 未指定なら両方 noop
810
740
  */
811
- function scopeDocumentCache(base, collection) {
812
- const itemKey = (slug) => `${collection}:${slug}`;
813
- let listSlot = null;
814
- let listInitialized = false;
815
- const mapInvalidateScope = (scope) => {
816
- if (scope === "all") return { collection };
817
- if ("slug" in scope) return {
818
- collection: scope.collection,
819
- slug: itemKey(scope.slug),
820
- kind: scope.kind
821
- };
822
- return scope;
823
- };
824
- return {
825
- name: `${base.name}@${collection}`,
826
- getList: async () => {
827
- if (!listInitialized) {
828
- listInitialized = true;
829
- listSlot = await base.getList();
830
- }
831
- return listSlot;
832
- },
833
- setList: (data) => {
834
- listSlot = data;
835
- listInitialized = true;
836
- return Promise.resolve();
837
- },
838
- getItemMeta: (slug) => base.getItemMeta(itemKey(slug)),
839
- setItemMeta: (slug, data) => base.setItemMeta(itemKey(slug), data),
840
- getItemContent: (slug) => base.getItemContent(itemKey(slug)),
841
- setItemContent: (slug, data) => base.setItemContent(itemKey(slug), data),
842
- async invalidate(scope) {
843
- const kind = scope === "all" ? "all" : scope.kind ?? "all";
844
- if (kind === "all" || kind === "meta") {
845
- listSlot = null;
846
- listInitialized = true;
847
- }
848
- if (!base.invalidate) return;
849
- return base.invalidate(mapInvalidateScope(scope));
741
+ function resolveCache(cache) {
742
+ const adapters = cache === void 0 ? [] : Array.isArray(cache) ? cache : [cache];
743
+ let doc = noopDocOps;
744
+ let docName = "noop-document";
745
+ let img = noopImgOps;
746
+ let imgName = "noop-image";
747
+ let docFound = false;
748
+ let imgFound = false;
749
+ for (const adapter of adapters) {
750
+ if (!docFound && adapter.handles.includes("document") && adapter.doc) {
751
+ doc = adapter.doc;
752
+ docName = adapter.name;
753
+ docFound = true;
850
754
  }
755
+ if (!imgFound && adapter.handles.includes("image") && adapter.img) {
756
+ img = adapter.img;
757
+ imgName = adapter.name;
758
+ imgFound = true;
759
+ }
760
+ }
761
+ return {
762
+ doc,
763
+ docName,
764
+ img,
765
+ imgName,
766
+ hasImg: imgFound
851
767
  };
852
768
  }
853
769
  const LOG_LEVEL_ORDER = {
@@ -870,68 +786,53 @@ function applyLogLevel(logger, minLevel) {
870
786
  return filtered;
871
787
  }
872
788
  /**
873
- * `preset` オプションを解決して `cache` / `renderer` のデフォルトを補完する内部関数。
874
- * 明示的な `cache` / `renderer` がある場合はそちらが優先される。
875
- * `preset` 未指定時は opts をそのまま返す。
876
- */
877
- function resolvePreset(opts) {
878
- if (opts.preset === "disabled") return {
879
- ...opts,
880
- cache: void 0
881
- };
882
- if (opts.preset === "node") {
883
- const presetResult = nodePreset({ ttlMs: opts.ttlMs });
884
- return {
885
- ...opts,
886
- cache: opts.cache ?? presetResult.cache,
887
- renderer: opts.renderer ?? presetResult.renderer
888
- };
889
- }
890
- return opts;
891
- }
892
- /**
893
- * 複数の DataSource を束ねた CMS クライアントを生成する。
789
+ * 複数の `CollectionDef` を束ねた CMS クライアントを生成する。
894
790
  *
895
- * @example
896
- * // Node.js(preset を使った簡潔な記法)
897
- * const cms = createCMS({ dataSources: cmsDataSources, preset: "node", ttlMs: 5 * 60_000 });
791
+ * 通常はユーザーが直接呼ぶことはなく、CLI 生成の `nhc.ts` の `createCMS`
792
+ * (低レベルのこの関数をラップしたもの) を経由する。
898
793
  *
899
794
  * @example
900
- * // 従来の spread パターン(引き続き動作する)
901
- * const cms = createCMS({ ...nodePreset({ ttlMs: 5 * 60_000 }), dataSources: cmsDataSources });
902
- *
903
- * @example
904
- * // キャッシュを細かく指定する場合
905
- * const cms = createCMS({
906
- * dataSources,
907
- * cache: { document, image, ttlMs: 60_000 },
795
+ * createCMS({
796
+ * collections: {
797
+ * posts: {
798
+ * source: createNotionCollection({ token, dataSourceId, properties }),
799
+ * slugField: "slug",
800
+ * statusField: "status",
801
+ * publishedStatuses: ["公開済み"],
802
+ * }
803
+ * },
804
+ * cache: memoryCache({ ttlMs: 5 * 60_000 }),
908
805
  * });
909
806
  */
910
807
  function createCMS(opts) {
911
- if (!opts.dataSources || Object.keys(opts.dataSources).length === 0) throw new CMSError({
808
+ if (!opts.collections || Object.keys(opts.collections).length === 0) throw new CMSError({
912
809
  code: "core/config_invalid",
913
- message: "createCMS: dataSources に少なくとも1つのコレクションを指定してください。",
810
+ message: "createCMS: collections に少なくとも 1 つのコレクションを指定してください。",
914
811
  context: { operation: "createCMS" }
915
812
  });
916
- for (const [name, col] of Object.entries(opts.collections ?? {})) {
917
- const c = col;
918
- if (c && !c.slug) throw new CMSError({
813
+ for (const [name, def] of Object.entries(opts.collections)) {
814
+ if (!def.source) throw new CMSError({
919
815
  code: "core/config_invalid",
920
- message: `createCMS: コレクション "${name}" の collections.slug は必須です。slug として使うフィールド名を指定してください。`,
816
+ message: `createCMS: コレクション "${name}" の source は必須です。`,
817
+ context: {
818
+ operation: "createCMS",
819
+ collection: name
820
+ }
821
+ });
822
+ if (!def.slugField) throw new CMSError({
823
+ code: "core/config_invalid",
824
+ message: `createCMS: コレクション "${name}" の slugField は必須です。`,
921
825
  context: {
922
826
  operation: "createCMS",
923
827
  collection: name
924
828
  }
925
829
  });
926
830
  }
927
- const resolved = resolvePreset(opts);
928
- const baseDocCache = resolveDocumentCache(resolved.cache);
929
- const imgCache = resolveImageCache(resolved.cache);
930
- const hasImageCache = hasImageCacheConfigured(resolved.cache);
931
- const ttlMs = resolveTtl(resolved.cache);
932
- const imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
831
+ const cacheRes = resolveCache(opts.cache);
832
+ const ttlMs = opts.ttlMs;
833
+ const imageProxyBase = opts.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
933
834
  const contentConfig = opts.content;
934
- const rendererFn = resolved.renderer;
835
+ const rendererFn = opts.renderer;
935
836
  const waitUntil = opts.waitUntil;
936
837
  const baseLogger = mergeLoggers(opts.plugins ?? [], opts.logger);
937
838
  const logger = opts.logLevel ? applyLogLevel(baseLogger, opts.logLevel) : baseLogger;
@@ -941,62 +842,59 @@ function createCMS(opts) {
941
842
  ...DEFAULT_RETRY_CONFIG,
942
843
  ...opts.rateLimiter ?? {}
943
844
  };
944
- const collectionNames = Object.keys(opts.dataSources);
845
+ const collectionNames = Object.keys(opts.collections);
945
846
  const collections = {};
946
- const scopedCaches = [];
947
847
  for (const name of collectionNames) {
948
- const source = opts.dataSources[name];
949
- const scopedCache = scopeDocumentCache(baseDocCache, name);
950
- scopedCaches.push(scopedCache);
951
- const col = opts.collections?.[name];
952
- const colHooks = col?.hooks;
848
+ const def = opts.collections[name];
849
+ const source = def.source;
850
+ const colHooks = def.hooks;
953
851
  const collectionHooks = colHooks ? mergeHooks([{
954
852
  name: `${name}:global`,
955
853
  hooks
956
854
  }], colHooks, logger) : hooks;
855
+ const renderCtx = {
856
+ source,
857
+ rendererFn,
858
+ imgCache: cacheRes.img,
859
+ imgCacheName: cacheRes.imgName,
860
+ hasImageCache: cacheRes.hasImg,
861
+ imageProxyBase,
862
+ contentConfig,
863
+ hooks: collectionHooks,
864
+ logger
865
+ };
957
866
  collections[name] = new CollectionClientImpl({
958
867
  collection: name,
959
868
  source,
960
- docCache: scopedCache,
961
- render: {
962
- source,
963
- rendererFn,
964
- imgCache,
965
- hasImageCache,
966
- imageProxyBase,
967
- contentConfig,
968
- hooks: collectionHooks,
969
- logger
970
- },
869
+ docCache: cacheRes.doc,
870
+ docCacheName: cacheRes.docName,
871
+ render: renderCtx,
971
872
  hooks: collectionHooks,
972
873
  logger,
973
874
  ttlMs,
974
- publishedStatuses: col?.publishedStatuses ? [...col.publishedStatuses] : [],
975
- accessibleStatuses: col?.accessibleStatuses ? [...col.accessibleStatuses] : [],
875
+ publishedStatuses: def.publishedStatuses ? [...def.publishedStatuses] : [],
876
+ accessibleStatuses: def.accessibleStatuses ? [...def.accessibleStatuses] : [],
976
877
  retryConfig,
977
878
  maxConcurrent,
978
879
  waitUntil,
979
- slugField: col?.slug
880
+ slugField: def.slugField
980
881
  });
981
882
  }
982
883
  const globalOps = {
983
884
  $collections: collectionNames,
984
- async $revalidate(scope) {
885
+ async $invalidate(scope) {
985
886
  logger?.debug?.("グローバルキャッシュを無効化", {
986
- operation: "$revalidate",
987
- cacheAdapter: baseDocCache.name
887
+ operation: "$invalidate",
888
+ cacheAdapter: cacheRes.docName
988
889
  });
989
- for (const cache of scopedCaches) {
990
- if (!cache.invalidate) continue;
991
- await cache.invalidate(scope ?? "all");
992
- }
890
+ await cacheRes.doc.invalidate(scope ?? "all");
993
891
  },
994
892
  $handler(handlerOpts) {
995
893
  return createHandler({
996
- imageCache: imgCache,
894
+ imageCache: cacheRes.img,
997
895
  parseWebhook: async (req, webhookSecret) => {
998
896
  for (const name of collectionNames) {
999
- const ds = opts.dataSources[name];
897
+ const ds = opts.collections[name].source;
1000
898
  if (ds.parseWebhook) try {
1001
899
  return await ds.parseWebhook(req.clone(), { secret: webhookSecret });
1002
900
  } catch (err) {
@@ -1016,11 +914,11 @@ function createCMS(opts) {
1016
914
  } catch {}
1017
915
  return null;
1018
916
  },
1019
- revalidate: (scope) => globalOps.$revalidate(scope)
917
+ revalidate: (scope) => globalOps.$invalidate(scope)
1020
918
  }, handlerOpts);
1021
919
  },
1022
920
  $getCachedImage(hash) {
1023
- return imgCache.get(hash);
921
+ return cacheRes.img.get(hash);
1024
922
  }
1025
923
  };
1026
924
  return Object.assign(Object.create(null), collections, globalOps);
@@ -1031,6 +929,6 @@ function definePlugin(plugin) {
1031
929
  return plugin;
1032
930
  }
1033
931
  //#endregion
1034
- export { CMSError, CollectionClientImpl, DEFAULT_RETRY_CONFIG, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale, memoryDocumentCache, memoryImageCache, mergeHooks, mergeLoggers, nodePreset, noopDocumentCache, noopImageCache, sha256Hex, withRetry };
932
+ export { CMSError, CollectionClientImpl, DEFAULT_RETRY_CONFIG, collectionKey, createCMS, createHandler, definePlugin, isCMSError, isCMSErrorInNamespace, isStale, memoryCache, mergeHooks, mergeLoggers, noopDocOps, noopImgOps, sha256Hex, withRetry };
1035
933
 
1036
934
  //# sourceMappingURL=index.mjs.map