@notion-headless-cms/core 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/cache/memory.d.mts +1 -1
- package/dist/cache/memory.mjs.map +1 -1
- package/dist/{content-Bffid8da.d.mts → content-D7PfY-bV.d.mts} +17 -7
- package/dist/errors-B5l-3w_F.d.mts +66 -0
- package/dist/errors.d.mts +2 -56
- package/dist/errors.mjs.map +1 -1
- package/dist/{hooks-CqqVxrYg.d.mts → hooks-CGOl6uP5.d.mts} +25 -15
- package/dist/hooks.d.mts +1 -1
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.d.mts +36 -19
- package/dist/index.mjs +198 -103
- package/dist/index.mjs.map +1 -1
- package/dist/{memory-CA1uTRbr.d.mts → memory-CxF7S-iB.d.mts} +3 -3
- package/package.json +2 -4
package/dist/index.mjs
CHANGED
|
@@ -59,44 +59,37 @@ const noopDocOps = noopDoc;
|
|
|
59
59
|
const noopImgOps = noopImg;
|
|
60
60
|
//#endregion
|
|
61
61
|
//#region src/image.ts
|
|
62
|
-
/** レスポンスヘッダまたはURLの拡張子からContent-Typeを推測する。 */
|
|
63
|
-
function inferContentType(url, responseContentType) {
|
|
64
|
-
if (responseContentType?.startsWith("image/")) return responseContentType.split(";")[0].trim();
|
|
65
|
-
if (url.includes(".png")) return "image/png";
|
|
66
|
-
if (url.includes(".gif")) return "image/gif";
|
|
67
|
-
if (url.includes(".webp")) return "image/webp";
|
|
68
|
-
return "image/jpeg";
|
|
69
|
-
}
|
|
70
62
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* メモリリーク防止に最大エントリ数を設けており、超過時は最古から削除する LRU。
|
|
63
|
+
* レスポンスの Content-Type ヘッダから画像の MIME タイプを取り出す。
|
|
64
|
+
* ヘッダがない、または image/* でない場合は CMSError を投げる。
|
|
65
|
+
* URL 拡張子からの推測や jpeg デフォルトは行わない。
|
|
76
66
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
67
|
+
function pickImageContentType(headerValue, notionUrl) {
|
|
68
|
+
if (!headerValue) throw new CMSError({
|
|
69
|
+
code: "cache/image_invalid_content_type",
|
|
70
|
+
message: "Image response missing Content-Type header.",
|
|
71
|
+
context: {
|
|
72
|
+
operation: "fetchAndCacheImage:contentType",
|
|
73
|
+
notionUrl
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const value = (headerValue.split(";")[0] ?? headerValue).trim().toLowerCase();
|
|
77
|
+
if (!value.startsWith("image/")) throw new CMSError({
|
|
78
|
+
code: "cache/image_invalid_content_type",
|
|
79
|
+
message: `Image response has non-image Content-Type: ${value}`,
|
|
80
|
+
context: {
|
|
81
|
+
operation: "fetchAndCacheImage:contentType",
|
|
82
|
+
notionUrl,
|
|
83
|
+
contentType: value
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return value;
|
|
93
87
|
}
|
|
94
88
|
/**
|
|
95
89
|
* Notion画像URLをfetchして ImageCacheOps にキャッシュし、プロキシURL を返す。
|
|
96
90
|
* 既存キャッシュがあれば再fetchしない。
|
|
97
91
|
*/
|
|
98
|
-
async function fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, logger) {
|
|
99
|
-
const hash = await memoSha256(notionUrl);
|
|
92
|
+
async function fetchAndCacheImage(cache, cacheName, notionUrl, hash, imageProxyBase, logger) {
|
|
100
93
|
const proxyUrl = `${imageProxyBase}/${hash}`;
|
|
101
94
|
if (await cache.get(hash)) {
|
|
102
95
|
logger?.debug?.("画像キャッシュヒット", {
|
|
@@ -123,7 +116,7 @@ async function fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, l
|
|
|
123
116
|
}
|
|
124
117
|
});
|
|
125
118
|
const data = await response.arrayBuffer();
|
|
126
|
-
const contentType =
|
|
119
|
+
const contentType = pickImageContentType(response.headers.get("content-type"), notionUrl);
|
|
127
120
|
await cache.set(hash, data, contentType);
|
|
128
121
|
logger?.debug?.("画像をキャッシュに保存", {
|
|
129
122
|
operation: "fetchAndCacheImage",
|
|
@@ -148,9 +141,20 @@ async function fetchAndCacheImage(cache, cacheName, notionUrl, imageProxyBase, l
|
|
|
148
141
|
* `ImageCacheOps` と `imageProxyBase` から `cacheImage` 関数を構築する。
|
|
149
142
|
* 返り値は Notion 画像 URL を受け取り、SHA-256 ハッシュをキャッシュキーとして
|
|
150
143
|
* {@link ImageCacheOps} に保存後、プロキシ URL を返す。
|
|
144
|
+
*
|
|
145
|
+
* ハッシュのメモ化はファクトリ呼び出し単位でスコープ化されており、
|
|
146
|
+
* インスタンス間でキャッシュを共有しない。
|
|
151
147
|
*/
|
|
152
148
|
function buildCacheImageFn(cache, cacheName, imageProxyBase, logger) {
|
|
153
|
-
|
|
149
|
+
const hashMemo = /* @__PURE__ */ new Map();
|
|
150
|
+
return async (notionUrl) => {
|
|
151
|
+
let hash = hashMemo.get(notionUrl);
|
|
152
|
+
if (hash === void 0) {
|
|
153
|
+
hash = await sha256Hex(notionUrl);
|
|
154
|
+
hashMemo.set(notionUrl, hash);
|
|
155
|
+
}
|
|
156
|
+
return fetchAndCacheImage(cache, cacheName, notionUrl, hash, imageProxyBase, logger);
|
|
157
|
+
};
|
|
154
158
|
}
|
|
155
159
|
//#endregion
|
|
156
160
|
//#region src/rendering.ts
|
|
@@ -191,21 +195,26 @@ async function buildCachedItemContent(item, ctx) {
|
|
|
191
195
|
}
|
|
192
196
|
});
|
|
193
197
|
}
|
|
194
|
-
let blocks
|
|
198
|
+
let blocks;
|
|
195
199
|
try {
|
|
196
200
|
blocks = await ctx.source.loadBlocks(item);
|
|
197
201
|
} catch (err) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
if (isCMSError(err)) throw err;
|
|
203
|
+
throw new CMSError({
|
|
204
|
+
code: "source/load_blocks_failed",
|
|
205
|
+
message: "Failed to load blocks from source.",
|
|
206
|
+
cause: err,
|
|
207
|
+
context: {
|
|
208
|
+
operation: "buildCachedItemContent:loadBlocks",
|
|
209
|
+
pageId: item.id,
|
|
210
|
+
slug: item.slug
|
|
211
|
+
}
|
|
201
212
|
});
|
|
202
|
-
blocks = [];
|
|
203
213
|
}
|
|
204
214
|
const cacheImage = ctx.hasImageCache ? buildCacheImageFn(ctx.imgCache, ctx.imgCacheName, ctx.imageProxyBase, ctx.logger) : void 0;
|
|
205
215
|
let html;
|
|
206
|
-
const rendererFn = ctx.rendererFn ?? await loadDefaultRenderer();
|
|
207
216
|
try {
|
|
208
|
-
html = await rendererFn(markdown, {
|
|
217
|
+
html = await ctx.rendererFn(markdown, {
|
|
209
218
|
imageProxyBase: ctx.imageProxyBase,
|
|
210
219
|
cacheImage,
|
|
211
220
|
remarkPlugins: ctx.contentConfig?.remarkPlugins,
|
|
@@ -241,23 +250,6 @@ async function buildCachedItemContent(item, ctx) {
|
|
|
241
250
|
ctx.hooks.onRenderEnd?.(item.slug, durationMs);
|
|
242
251
|
return result;
|
|
243
252
|
}
|
|
244
|
-
/**
|
|
245
|
-
* renderer オプション未指定時のフォールバック。
|
|
246
|
-
* @notion-headless-cms/renderer を動的 import する。
|
|
247
|
-
* createCMS({ renderer }) で明示注入された場合はこのパスを通らない。
|
|
248
|
-
*/
|
|
249
|
-
async function loadDefaultRenderer() {
|
|
250
|
-
try {
|
|
251
|
-
return (await import("@notion-headless-cms/renderer")).renderMarkdown;
|
|
252
|
-
} catch (err) {
|
|
253
|
-
throw new CMSError({
|
|
254
|
-
code: "renderer/failed",
|
|
255
|
-
message: "renderer オプションが未指定で @notion-headless-cms/renderer が見つかりません。 createCMS({ renderer }) でレンダラーを注入するか、@notion-headless-cms/renderer をインストールしてください。",
|
|
256
|
-
cause: err,
|
|
257
|
-
context: { operation: "loadDefaultRenderer" }
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
253
|
//#endregion
|
|
262
254
|
//#region src/retry.ts
|
|
263
255
|
const DEFAULT_RETRY_CONFIG = {
|
|
@@ -376,7 +368,7 @@ var CollectionClientImpl = class {
|
|
|
376
368
|
async check(slug, currentVersion) {
|
|
377
369
|
const raw = await this.findRaw(slug);
|
|
378
370
|
if (!raw) return null;
|
|
379
|
-
if (raw.
|
|
371
|
+
if (raw.lastEditedTime === currentVersion) return { stale: false };
|
|
380
372
|
const meta = await this.persistMeta(slug, raw);
|
|
381
373
|
await this.invalidateContent(slug);
|
|
382
374
|
return {
|
|
@@ -478,16 +470,31 @@ var CollectionClientImpl = class {
|
|
|
478
470
|
this.ctx.hooks.onContentRevalidated?.(slug, fresh);
|
|
479
471
|
return fresh;
|
|
480
472
|
}
|
|
481
|
-
/**
|
|
473
|
+
/** メタ既知の状態で本文だけバックグラウンド再生成する。エラーは onSwrError フックに通知する。 */
|
|
482
474
|
async rebuildContentBg(slug, item) {
|
|
483
475
|
try {
|
|
484
476
|
const fresh = await buildCachedItemContent(item, this.ctx.render);
|
|
485
477
|
await this.ctx.docCache.setContent(this.ctx.collection, slug, fresh);
|
|
486
478
|
this.ctx.hooks.onContentRevalidated?.(slug, fresh);
|
|
487
479
|
} catch (err) {
|
|
480
|
+
const cmsErr = isCMSError(err) ? err : new CMSError({
|
|
481
|
+
code: "swr/content_rebuild_failed",
|
|
482
|
+
message: "SWR background content rebuild failed.",
|
|
483
|
+
cause: err,
|
|
484
|
+
context: {
|
|
485
|
+
operation: "swr.rebuildContentBg",
|
|
486
|
+
collection: this.ctx.collection,
|
|
487
|
+
slug
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
this.ctx.hooks.onSwrError?.(cmsErr, {
|
|
491
|
+
phase: "item-content",
|
|
492
|
+
slug
|
|
493
|
+
});
|
|
488
494
|
this.ctx.logger?.warn?.("本文のバックグラウンド再生成に失敗", {
|
|
489
495
|
slug,
|
|
490
496
|
collection: this.ctx.collection,
|
|
497
|
+
code: cmsErr.code,
|
|
491
498
|
error: err instanceof Error ? err.message : String(err)
|
|
492
499
|
});
|
|
493
500
|
}
|
|
@@ -576,9 +583,24 @@ var CollectionClientImpl = class {
|
|
|
576
583
|
});
|
|
577
584
|
}
|
|
578
585
|
} catch (err) {
|
|
586
|
+
const cmsErr = isCMSError(err) ? err : new CMSError({
|
|
587
|
+
code: "swr/item_check_failed",
|
|
588
|
+
message: "SWR background item check failed.",
|
|
589
|
+
cause: err,
|
|
590
|
+
context: {
|
|
591
|
+
operation: "swr.checkAndUpdateItemBg",
|
|
592
|
+
collection: this.ctx.collection,
|
|
593
|
+
slug
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
this.ctx.hooks.onSwrError?.(cmsErr, {
|
|
597
|
+
phase: "item-meta",
|
|
598
|
+
slug
|
|
599
|
+
});
|
|
579
600
|
this.ctx.logger?.warn?.("SWR: アイテムのバックグラウンド差分チェックに失敗", {
|
|
580
601
|
slug,
|
|
581
602
|
collection: this.ctx.collection,
|
|
603
|
+
code: cmsErr.code,
|
|
582
604
|
error: err instanceof Error ? err.message : String(err)
|
|
583
605
|
});
|
|
584
606
|
}
|
|
@@ -608,14 +630,25 @@ var CollectionClientImpl = class {
|
|
|
608
630
|
});
|
|
609
631
|
}
|
|
610
632
|
} catch (err) {
|
|
633
|
+
const cmsErr = isCMSError(err) ? err : new CMSError({
|
|
634
|
+
code: "swr/list_check_failed",
|
|
635
|
+
message: "SWR background list check failed.",
|
|
636
|
+
cause: err,
|
|
637
|
+
context: {
|
|
638
|
+
operation: "swr.checkAndUpdateListBg",
|
|
639
|
+
collection: this.ctx.collection
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
this.ctx.hooks.onSwrError?.(cmsErr, { phase: "list" });
|
|
611
643
|
this.ctx.logger?.warn?.("SWR: リストのバックグラウンド差分チェックに失敗", {
|
|
612
644
|
collection: this.ctx.collection,
|
|
645
|
+
code: cmsErr.code,
|
|
613
646
|
error: err instanceof Error ? err.message : String(err)
|
|
614
647
|
});
|
|
615
648
|
}
|
|
616
649
|
}
|
|
617
|
-
fetchListRaw() {
|
|
618
|
-
return withRetry(() => this.ctx.source.list({ publishedStatuses: this.ctx.publishedStatuses.length > 0 ? this.ctx.publishedStatuses : void 0 }), {
|
|
650
|
+
async fetchListRaw() {
|
|
651
|
+
return (await withRetry(() => this.ctx.source.list({ publishedStatuses: this.ctx.publishedStatuses.length > 0 ? this.ctx.publishedStatuses : void 0 }), {
|
|
619
652
|
...this.ctx.retryConfig,
|
|
620
653
|
onRetry: (attempt, status) => {
|
|
621
654
|
this.ctx.logger?.warn?.("list() リトライ中", {
|
|
@@ -623,6 +656,10 @@ var CollectionClientImpl = class {
|
|
|
623
656
|
status
|
|
624
657
|
});
|
|
625
658
|
}
|
|
659
|
+
})).filter((item) => {
|
|
660
|
+
if (item.isArchived || item.isInTrash) return false;
|
|
661
|
+
if (this.ctx.accessibleStatuses.length > 0 && (!item.status || !this.ctx.accessibleStatuses.includes(item.status))) return false;
|
|
662
|
+
return true;
|
|
626
663
|
});
|
|
627
664
|
}
|
|
628
665
|
async findRaw(slug) {
|
|
@@ -642,12 +679,23 @@ var CollectionClientImpl = class {
|
|
|
642
679
|
if (notionPropName && findByProp) item = await withRetry(() => findByProp(notionPropName, slug), retryOpts);
|
|
643
680
|
else item = (await withRetry(() => this.ctx.source.list(), retryOpts)).find((i) => i.slug === slug) ?? null;
|
|
644
681
|
if (!item) return null;
|
|
682
|
+
if (item.isArchived || item.isInTrash) return null;
|
|
645
683
|
if (this.ctx.accessibleStatuses.length > 0 && (!item.status || !this.ctx.accessibleStatuses.includes(item.status))) return null;
|
|
646
684
|
return item;
|
|
647
685
|
}
|
|
648
686
|
};
|
|
687
|
+
function matchesWhere(item, where) {
|
|
688
|
+
for (const key of Object.keys(where)) {
|
|
689
|
+
const expected = where[key];
|
|
690
|
+
const actual = item[key];
|
|
691
|
+
if (Array.isArray(expected)) {
|
|
692
|
+
if (!expected.includes(actual)) return false;
|
|
693
|
+
} else if (actual !== expected) return false;
|
|
694
|
+
}
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
649
697
|
function applyListOptions(items, opts) {
|
|
650
|
-
if (!opts) return items;
|
|
698
|
+
if (!opts) return sortByPublishedAtDesc(items);
|
|
651
699
|
let result = items;
|
|
652
700
|
if (opts.status) {
|
|
653
701
|
const allow = new Set(Array.isArray(opts.status) ? opts.status : [opts.status]);
|
|
@@ -662,24 +710,46 @@ function applyListOptions(items, opts) {
|
|
|
662
710
|
}
|
|
663
711
|
if (opts.where) {
|
|
664
712
|
const where = opts.where;
|
|
665
|
-
result = result.filter((it) =>
|
|
713
|
+
result = result.filter((it) => matchesWhere(it, where));
|
|
666
714
|
}
|
|
715
|
+
if (opts.filter) result = result.filter(opts.filter);
|
|
667
716
|
if (opts.sort) result = [...result].sort(makeComparator(opts.sort));
|
|
717
|
+
else result = sortByPublishedAtDesc(result);
|
|
668
718
|
const skip = opts.skip ?? 0;
|
|
669
719
|
const limit = opts.limit;
|
|
670
720
|
if (skip > 0 || limit !== void 0) result = result.slice(skip, limit !== void 0 ? skip + limit : void 0);
|
|
671
721
|
return result;
|
|
672
722
|
}
|
|
723
|
+
/** publishedAt 降順、未設定の場合は lastEditedTime 降順でソートする。 */
|
|
724
|
+
function sortByPublishedAtDesc(items) {
|
|
725
|
+
return [...items].sort((a, b) => {
|
|
726
|
+
const av = a.publishedAt ?? a.lastEditedTime;
|
|
727
|
+
const bv = b.publishedAt ?? b.lastEditedTime;
|
|
728
|
+
if (av === bv) return 0;
|
|
729
|
+
return av > bv ? -1 : 1;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
673
732
|
function makeComparator(sort) {
|
|
733
|
+
if (sort.compare) return sort.compare;
|
|
674
734
|
const by = sort.by;
|
|
675
735
|
const dir = sort.dir === "asc" ? 1 : -1;
|
|
676
736
|
return (a, b) => {
|
|
677
737
|
const av = a[by];
|
|
678
738
|
const bv = b[by];
|
|
679
739
|
if (av === bv) return 0;
|
|
680
|
-
if (av === void 0) return 1;
|
|
681
|
-
if (bv === void 0) return -1;
|
|
682
|
-
return av > bv ? dir : -dir;
|
|
740
|
+
if (av === void 0 || av === null) return 1;
|
|
741
|
+
if (bv === void 0 || bv === null) return -1;
|
|
742
|
+
if (typeof av === "string" && typeof bv === "string") return av > bv ? dir : -dir;
|
|
743
|
+
if (typeof av === "number" && typeof bv === "number") return av > bv ? dir : -dir;
|
|
744
|
+
throw new CMSError({
|
|
745
|
+
code: "core/sort_unsupported_type",
|
|
746
|
+
message: `"${String(by)}" フィールドの型 "${typeof av}" はソート非対応です。compare 関数を指定してください。`,
|
|
747
|
+
context: {
|
|
748
|
+
operation: "makeComparator",
|
|
749
|
+
field: String(by),
|
|
750
|
+
type: typeof av
|
|
751
|
+
}
|
|
752
|
+
});
|
|
683
753
|
};
|
|
684
754
|
}
|
|
685
755
|
//#endregion
|
|
@@ -690,12 +760,23 @@ const DEFAULT_OPTS = {
|
|
|
690
760
|
revalidatePath: "/revalidate"
|
|
691
761
|
};
|
|
692
762
|
/**
|
|
763
|
+
* CMSError のコードから HTTP ステータスコードを返す。
|
|
764
|
+
* 既知の webhook エラーコードのみ対応し、それ以外は null を返す。
|
|
765
|
+
*/
|
|
766
|
+
function webhookErrorStatus(code) {
|
|
767
|
+
if (code === "webhook/signature_invalid") return 401;
|
|
768
|
+
if (code === "webhook/not_implemented") return 501;
|
|
769
|
+
if (code === "webhook/unknown_collection") return 404;
|
|
770
|
+
if (code === "webhook/payload_invalid") return 400;
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
693
774
|
* Web Standard な Request → Response ルーター。
|
|
694
775
|
* Next.js / React Router / Hono / Cloudflare Workers いずれでも使える。
|
|
695
776
|
*
|
|
696
777
|
* ルート:
|
|
697
|
-
* - GET `{basePath}/images/:hash`
|
|
698
|
-
* - POST `{basePath}/revalidate`
|
|
778
|
+
* - GET `{basePath}/images/:hash` — 画像プロキシ
|
|
779
|
+
* - POST `{basePath}/revalidate/:collection` — Webhook 受信 + $revalidate()
|
|
699
780
|
*/
|
|
700
781
|
function createHandler(adapter, opts = {}) {
|
|
701
782
|
const basePath = trimTrailingSlash(opts.basePath ?? DEFAULT_OPTS.basePath);
|
|
@@ -715,23 +796,38 @@ function createHandler(adapter, opts = {}) {
|
|
|
715
796
|
headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
716
797
|
return new Response(object.data, { headers });
|
|
717
798
|
}
|
|
718
|
-
if (req.method === "POST" && rel
|
|
719
|
-
const
|
|
720
|
-
if (!
|
|
799
|
+
if (req.method === "POST" && rel.startsWith(`${revalidatePath}/`)) {
|
|
800
|
+
const collection = rel.slice(revalidatePath.length + 1);
|
|
801
|
+
if (!collection || collection.includes("/")) return new Response(JSON.stringify({
|
|
721
802
|
ok: false,
|
|
722
|
-
reason: "
|
|
803
|
+
reason: "collection required"
|
|
723
804
|
}), {
|
|
724
805
|
status: 400,
|
|
725
806
|
headers: { "content-type": "application/json" }
|
|
726
807
|
});
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
808
|
+
try {
|
|
809
|
+
const scope = await adapter.parseWebhookFor(collection, req, opts.webhookSecret);
|
|
810
|
+
await adapter.revalidate(scope);
|
|
811
|
+
return new Response(JSON.stringify({
|
|
812
|
+
ok: true,
|
|
813
|
+
scope
|
|
814
|
+
}), {
|
|
815
|
+
status: 200,
|
|
816
|
+
headers: { "content-type": "application/json" }
|
|
817
|
+
});
|
|
818
|
+
} catch (err) {
|
|
819
|
+
if (isCMSError(err)) {
|
|
820
|
+
const status = webhookErrorStatus(err.code);
|
|
821
|
+
if (status !== null) return new Response(JSON.stringify({
|
|
822
|
+
ok: false,
|
|
823
|
+
code: err.code
|
|
824
|
+
}), {
|
|
825
|
+
status,
|
|
826
|
+
headers: { "content-type": "application/json" }
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
throw err;
|
|
830
|
+
}
|
|
735
831
|
}
|
|
736
832
|
return new Response("Not Found", { status: 404 });
|
|
737
833
|
};
|
|
@@ -853,10 +949,10 @@ function createCMS(opts) {
|
|
|
853
949
|
...DEFAULT_RETRY_CONFIG,
|
|
854
950
|
...opts.rateLimiter ?? {}
|
|
855
951
|
};
|
|
856
|
-
const collectionNames =
|
|
952
|
+
const collectionNames = [];
|
|
857
953
|
const collections = {};
|
|
858
|
-
for (const name of
|
|
859
|
-
|
|
954
|
+
for (const [name, def] of Object.entries(opts.collections)) {
|
|
955
|
+
collectionNames.push(name);
|
|
860
956
|
const source = def.source;
|
|
861
957
|
const colHooks = def.hooks;
|
|
862
958
|
const collectionHooks = colHooks ? mergeHooks([{
|
|
@@ -903,27 +999,26 @@ function createCMS(opts) {
|
|
|
903
999
|
$handler(handlerOpts) {
|
|
904
1000
|
return createHandler({
|
|
905
1001
|
imageCache: cacheRes.img,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1002
|
+
async parseWebhookFor(collection, req, webhookSecret) {
|
|
1003
|
+
const def = opts.collections[collection];
|
|
1004
|
+
if (!def) throw new CMSError({
|
|
1005
|
+
code: "webhook/unknown_collection",
|
|
1006
|
+
message: `Unknown collection: ${collection}`,
|
|
1007
|
+
context: {
|
|
1008
|
+
operation: "parseWebhookFor",
|
|
1009
|
+
collection
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
const ds = def.source;
|
|
1013
|
+
if (!ds.parseWebhook) throw new CMSError({
|
|
1014
|
+
code: "webhook/not_implemented",
|
|
1015
|
+
message: `Collection "${collection}" does not support webhooks.`,
|
|
1016
|
+
context: {
|
|
1017
|
+
operation: "parseWebhookFor",
|
|
1018
|
+
collection
|
|
916
1019
|
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const body = await req.json();
|
|
920
|
-
if (body.slug && body.collection) return {
|
|
921
|
-
collection: body.collection,
|
|
922
|
-
slug: body.slug
|
|
923
|
-
};
|
|
924
|
-
if (body.collection) return { collection: body.collection };
|
|
925
|
-
} catch {}
|
|
926
|
-
return null;
|
|
1020
|
+
});
|
|
1021
|
+
return ds.parseWebhook(req, { secret: webhookSecret });
|
|
927
1022
|
},
|
|
928
1023
|
revalidate: (scope) => globalOps.$invalidate(scope)
|
|
929
1024
|
}, handlerOpts);
|