@notion-headless-cms/core 0.1.0 → 0.1.2
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 +145 -47
- package/dist/cache/memory.d.ts +54 -0
- package/dist/cache/memory.js +15 -0
- package/dist/cache/memory.js.map +1 -0
- package/dist/cache/noop.d.ts +9 -0
- package/dist/cache/noop.js +9 -0
- package/dist/cache/noop.js.map +1 -0
- package/dist/cache-DvbyemBK.d.ts +33 -0
- package/dist/chunk-4KGKWKKI.js +80 -0
- package/dist/chunk-4KGKWKKI.js.map +1 -0
- package/dist/chunk-6DG63XUF.js +42 -0
- package/dist/chunk-6DG63XUF.js.map +1 -0
- package/dist/chunk-6LHROEPI.js +104 -0
- package/dist/chunk-6LHROEPI.js.map +1 -0
- package/dist/chunk-V6ML4QE5.js +26 -0
- package/dist/chunk-V6ML4QE5.js.map +1 -0
- package/dist/content-Biqf0l_o.d.ts +51 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/hooks-B83RUclt.d.ts +41 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +9 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +171 -185
- package/dist/index.js +373 -276
- package/dist/index.js.map +1 -0
- package/package.json +29 -11
package/dist/index.js
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CMSError,
|
|
3
|
+
isCMSError,
|
|
4
|
+
isCMSErrorInNamespace
|
|
5
|
+
} from "./chunk-V6ML4QE5.js";
|
|
6
|
+
import {
|
|
7
|
+
mergeHooks,
|
|
8
|
+
mergeLoggers
|
|
9
|
+
} from "./chunk-4KGKWKKI.js";
|
|
10
|
+
import {
|
|
11
|
+
memoryCache,
|
|
12
|
+
memoryDocumentCache,
|
|
13
|
+
memoryImageCache
|
|
14
|
+
} from "./chunk-6LHROEPI.js";
|
|
15
|
+
import {
|
|
16
|
+
noopDocumentCache,
|
|
17
|
+
noopImageCache
|
|
18
|
+
} from "./chunk-6DG63XUF.js";
|
|
19
|
+
|
|
1
20
|
// src/cache.ts
|
|
2
21
|
async function sha256Hex(input) {
|
|
3
22
|
const data = new TextEncoder().encode(input);
|
|
@@ -9,112 +28,6 @@ function isStale(cachedAt, ttlMs) {
|
|
|
9
28
|
return Date.now() - cachedAt > ttlMs;
|
|
10
29
|
}
|
|
11
30
|
|
|
12
|
-
// src/cache/memory.ts
|
|
13
|
-
var MemoryDocumentCache = class {
|
|
14
|
-
name = "memory-document";
|
|
15
|
-
list = null;
|
|
16
|
-
items = /* @__PURE__ */ new Map();
|
|
17
|
-
getList() {
|
|
18
|
-
return Promise.resolve(this.list);
|
|
19
|
-
}
|
|
20
|
-
setList(data) {
|
|
21
|
-
this.list = data;
|
|
22
|
-
return Promise.resolve();
|
|
23
|
-
}
|
|
24
|
-
getItem(slug) {
|
|
25
|
-
return Promise.resolve(this.items.get(slug) ?? null);
|
|
26
|
-
}
|
|
27
|
-
setItem(slug, data) {
|
|
28
|
-
this.items.set(slug, data);
|
|
29
|
-
return Promise.resolve();
|
|
30
|
-
}
|
|
31
|
-
async invalidate(scope) {
|
|
32
|
-
if (scope === "all") {
|
|
33
|
-
this.list = null;
|
|
34
|
-
this.items.clear();
|
|
35
|
-
} else if ("slug" in scope) {
|
|
36
|
-
this.items.delete(scope.slug);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
var MemoryImageCache = class {
|
|
41
|
-
name = "memory-image";
|
|
42
|
-
store = /* @__PURE__ */ new Map();
|
|
43
|
-
get(hash) {
|
|
44
|
-
return Promise.resolve(this.store.get(hash) ?? null);
|
|
45
|
-
}
|
|
46
|
-
set(hash, data, contentType) {
|
|
47
|
-
this.store.set(hash, { data, contentType });
|
|
48
|
-
return Promise.resolve();
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
function memoryDocumentCache() {
|
|
52
|
-
return new MemoryDocumentCache();
|
|
53
|
-
}
|
|
54
|
-
function memoryImageCache() {
|
|
55
|
-
return new MemoryImageCache();
|
|
56
|
-
}
|
|
57
|
-
function memoryCache() {
|
|
58
|
-
return new MemoryDocumentCache();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// src/cache/noop.ts
|
|
62
|
-
var NoopDocumentCache = class {
|
|
63
|
-
name = "noop-document";
|
|
64
|
-
getList() {
|
|
65
|
-
return Promise.resolve(null);
|
|
66
|
-
}
|
|
67
|
-
setList(_data) {
|
|
68
|
-
return Promise.resolve();
|
|
69
|
-
}
|
|
70
|
-
getItem(_slug) {
|
|
71
|
-
return Promise.resolve(null);
|
|
72
|
-
}
|
|
73
|
-
setItem(_slug, _data) {
|
|
74
|
-
return Promise.resolve();
|
|
75
|
-
}
|
|
76
|
-
invalidate(_scope) {
|
|
77
|
-
return Promise.resolve();
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
var NoopImageCache = class {
|
|
81
|
-
name = "noop-image";
|
|
82
|
-
get(_hash) {
|
|
83
|
-
return Promise.resolve(null);
|
|
84
|
-
}
|
|
85
|
-
set(_hash, _data, _contentType) {
|
|
86
|
-
return Promise.resolve();
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
var _noopDocument = new NoopDocumentCache();
|
|
90
|
-
var _noopImage = new NoopImageCache();
|
|
91
|
-
function noopDocumentCache() {
|
|
92
|
-
return _noopDocument;
|
|
93
|
-
}
|
|
94
|
-
function noopImageCache() {
|
|
95
|
-
return _noopImage;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/cms.ts
|
|
99
|
-
import { renderMarkdown } from "@notion-headless-cms/renderer";
|
|
100
|
-
|
|
101
|
-
// src/errors.ts
|
|
102
|
-
var CMSError = class extends Error {
|
|
103
|
-
code;
|
|
104
|
-
cause;
|
|
105
|
-
context;
|
|
106
|
-
constructor(params) {
|
|
107
|
-
super(params.message, { cause: params.cause });
|
|
108
|
-
this.name = "CMSError";
|
|
109
|
-
this.code = params.code;
|
|
110
|
-
this.cause = params.cause;
|
|
111
|
-
this.context = params.context;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
function isCMSError(error) {
|
|
115
|
-
return error instanceof CMSError;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
31
|
// src/image.ts
|
|
119
32
|
function inferContentType(url, responseContentType) {
|
|
120
33
|
if (responseContentType?.startsWith("image/")) {
|
|
@@ -134,7 +47,17 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
|
|
|
134
47
|
const response = await fetch(notionUrl, {
|
|
135
48
|
signal: AbortSignal.timeout(1e4)
|
|
136
49
|
});
|
|
137
|
-
if (!response.ok)
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new CMSError({
|
|
52
|
+
code: "cache/image_fetch_failed",
|
|
53
|
+
message: `Failed to fetch Notion image: HTTP ${response.status}`,
|
|
54
|
+
context: {
|
|
55
|
+
operation: "fetchAndCacheImage",
|
|
56
|
+
notionUrl,
|
|
57
|
+
httpStatus: response.status
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
138
61
|
const data = await response.arrayBuffer();
|
|
139
62
|
const contentType = inferContentType(
|
|
140
63
|
notionUrl,
|
|
@@ -142,8 +65,9 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
|
|
|
142
65
|
);
|
|
143
66
|
await cache.set(hash, data, contentType);
|
|
144
67
|
} catch (err) {
|
|
68
|
+
if (isCMSError(err)) throw err;
|
|
145
69
|
throw new CMSError({
|
|
146
|
-
code: "
|
|
70
|
+
code: "cache/io_failed",
|
|
147
71
|
message: "Failed to fetch or cache Notion image.",
|
|
148
72
|
cause: err,
|
|
149
73
|
context: { operation: "fetchAndCacheImage", notionUrl }
|
|
@@ -155,23 +79,181 @@ function buildCacheImageFn(cache, imageProxyBase) {
|
|
|
155
79
|
return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase);
|
|
156
80
|
}
|
|
157
81
|
|
|
82
|
+
// src/query.ts
|
|
83
|
+
var QueryBuilder = class {
|
|
84
|
+
source;
|
|
85
|
+
defaultStatuses;
|
|
86
|
+
_statuses = [];
|
|
87
|
+
_tags = [];
|
|
88
|
+
_predicate;
|
|
89
|
+
_sortField;
|
|
90
|
+
_sortDir = "asc";
|
|
91
|
+
_page = 1;
|
|
92
|
+
_perPage = 20;
|
|
93
|
+
constructor(source, defaultStatuses = []) {
|
|
94
|
+
this.source = source;
|
|
95
|
+
this.defaultStatuses = defaultStatuses;
|
|
96
|
+
}
|
|
97
|
+
status(s) {
|
|
98
|
+
this._statuses = Array.isArray(s) ? s : [s];
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
tag(t) {
|
|
102
|
+
this._tags = Array.isArray(t) ? t : [t];
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
where(predicate) {
|
|
106
|
+
this._predicate = predicate;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
sortBy(field, dir = "asc") {
|
|
110
|
+
this._sortField = field;
|
|
111
|
+
this._sortDir = dir;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
paginate(opts) {
|
|
115
|
+
this._page = opts.page;
|
|
116
|
+
this._perPage = opts.perPage;
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
async execute() {
|
|
120
|
+
const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
|
|
121
|
+
if (this.source.query && !this._predicate) {
|
|
122
|
+
const result = await this.source.query({
|
|
123
|
+
filter: {
|
|
124
|
+
statuses,
|
|
125
|
+
tags: this._tags.length > 0 ? this._tags : void 0
|
|
126
|
+
},
|
|
127
|
+
sort: this._sortField ? [{ property: this._sortField, direction: this._sortDir }] : void 0,
|
|
128
|
+
pageSize: this._perPage
|
|
129
|
+
});
|
|
130
|
+
const items2 = result.items;
|
|
131
|
+
return {
|
|
132
|
+
items: items2,
|
|
133
|
+
total: items2.length,
|
|
134
|
+
page: this._page,
|
|
135
|
+
perPage: this._perPage,
|
|
136
|
+
hasNext: result.hasMore,
|
|
137
|
+
hasPrev: this._page > 1
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
let items = await this.source.list({
|
|
141
|
+
publishedStatuses: statuses
|
|
142
|
+
});
|
|
143
|
+
if (this._tags.length > 0) {
|
|
144
|
+
items = items.filter((item) => {
|
|
145
|
+
const itemTags = item.tags;
|
|
146
|
+
if (!Array.isArray(itemTags)) return false;
|
|
147
|
+
return this._tags.some((tag) => itemTags.includes(tag));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (this._predicate) {
|
|
151
|
+
items = items.filter(this._predicate);
|
|
152
|
+
}
|
|
153
|
+
if (this._sortField) {
|
|
154
|
+
const field = this._sortField;
|
|
155
|
+
const dir = this._sortDir;
|
|
156
|
+
items = [...items].sort((a, b) => {
|
|
157
|
+
const av = a[field];
|
|
158
|
+
const bv = b[field];
|
|
159
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
160
|
+
return dir === "asc" ? cmp : -cmp;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const total = items.length;
|
|
164
|
+
const start = (this._page - 1) * this._perPage;
|
|
165
|
+
const paged = items.slice(start, start + this._perPage);
|
|
166
|
+
return {
|
|
167
|
+
items: paged,
|
|
168
|
+
total,
|
|
169
|
+
page: this._page,
|
|
170
|
+
perPage: this._perPage,
|
|
171
|
+
hasNext: start + this._perPage < total,
|
|
172
|
+
hasPrev: this._page > 1
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async executeOne() {
|
|
176
|
+
const result = await this.execute();
|
|
177
|
+
return result.items[0] ?? null;
|
|
178
|
+
}
|
|
179
|
+
/** 前後アイテムを返す。sortBy() で指定したソート順を適用する。 */
|
|
180
|
+
async adjacent(slug) {
|
|
181
|
+
const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
|
|
182
|
+
let items = await this.source.list({ publishedStatuses: statuses });
|
|
183
|
+
if (this._sortField) {
|
|
184
|
+
const field = this._sortField;
|
|
185
|
+
const dir = this._sortDir;
|
|
186
|
+
items = [...items].sort((a, b) => {
|
|
187
|
+
const av = a[field];
|
|
188
|
+
const bv = b[field];
|
|
189
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
190
|
+
return dir === "asc" ? cmp : -cmp;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const idx = items.findIndex((item) => item.slug === slug);
|
|
194
|
+
if (idx === -1) return { prev: null, next: null };
|
|
195
|
+
return {
|
|
196
|
+
prev: idx > 0 ? items[idx - 1] : null,
|
|
197
|
+
next: idx < items.length - 1 ? items[idx + 1] : null
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** 最初の 1 件を返す。`.paginate({ page: 1, perPage: 1 }).executeOne()` の短縮形。 */
|
|
201
|
+
first() {
|
|
202
|
+
return this.paginate({ page: 1, perPage: 1 }).executeOne();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/retry.ts
|
|
207
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
208
|
+
retryOn: [429, 502, 503],
|
|
209
|
+
maxRetries: 4,
|
|
210
|
+
baseDelayMs: 1e3,
|
|
211
|
+
jitter: true
|
|
212
|
+
};
|
|
213
|
+
async function withRetry(fn, config) {
|
|
214
|
+
let lastError;
|
|
215
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
216
|
+
try {
|
|
217
|
+
return await fn();
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const status = err.status;
|
|
220
|
+
if (status === void 0 || !config.retryOn.includes(status)) {
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
lastError = err;
|
|
224
|
+
if (attempt < config.maxRetries) {
|
|
225
|
+
config.onRetry?.(attempt + 1, status);
|
|
226
|
+
const jitterFactor = config.jitter !== false ? 0.5 + Math.random() * 0.5 : 1;
|
|
227
|
+
const delay = config.baseDelayMs * 2 ** attempt * jitterFactor;
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastError;
|
|
233
|
+
}
|
|
234
|
+
|
|
158
235
|
// src/cms.ts
|
|
159
236
|
var DEFAULT_IMAGE_PROXY_BASE = "/api/images";
|
|
160
|
-
function buildListVersion(items) {
|
|
161
|
-
return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
|
|
162
|
-
}
|
|
163
237
|
function resolveDocumentCache(cache) {
|
|
164
|
-
if (!cache || cache
|
|
238
|
+
if (!cache || cache === "disabled" || !cache.document) {
|
|
165
239
|
return noopDocumentCache();
|
|
166
240
|
}
|
|
167
241
|
return cache.document;
|
|
168
242
|
}
|
|
169
243
|
function resolveImageCache(cache) {
|
|
170
|
-
if (!cache || cache
|
|
244
|
+
if (!cache || cache === "disabled" || !cache.image) {
|
|
171
245
|
return noopImageCache();
|
|
172
246
|
}
|
|
173
247
|
return cache.image;
|
|
174
248
|
}
|
|
249
|
+
function resolveTtl(cache) {
|
|
250
|
+
if (!cache || cache === "disabled") return void 0;
|
|
251
|
+
return cache.ttlMs;
|
|
252
|
+
}
|
|
253
|
+
function hasImageCacheConfigured(cache) {
|
|
254
|
+
if (!cache || cache === "disabled") return false;
|
|
255
|
+
return !!cache.image;
|
|
256
|
+
}
|
|
175
257
|
var CMS = class {
|
|
176
258
|
source;
|
|
177
259
|
docCache;
|
|
@@ -182,100 +264,154 @@ var CMS = class {
|
|
|
182
264
|
accessibleStatuses;
|
|
183
265
|
imageProxyBase;
|
|
184
266
|
contentConfig;
|
|
267
|
+
rendererFn;
|
|
185
268
|
waitUntil;
|
|
269
|
+
hooks;
|
|
270
|
+
logger;
|
|
271
|
+
retryConfig;
|
|
272
|
+
maxConcurrent;
|
|
273
|
+
cache;
|
|
186
274
|
constructor(opts) {
|
|
187
275
|
this.source = opts.source;
|
|
188
276
|
this.docCache = resolveDocumentCache(opts.cache);
|
|
189
277
|
this.imgCache = resolveImageCache(opts.cache);
|
|
190
|
-
this.hasImageCache =
|
|
191
|
-
this.ttlMs = opts.cache
|
|
278
|
+
this.hasImageCache = hasImageCacheConfigured(opts.cache);
|
|
279
|
+
this.ttlMs = resolveTtl(opts.cache);
|
|
192
280
|
this.publishedStatuses = opts.schema?.publishedStatuses ?? (opts.source.publishedStatuses ? [...opts.source.publishedStatuses] : []);
|
|
193
281
|
this.accessibleStatuses = opts.schema?.accessibleStatuses ?? (opts.source.accessibleStatuses ? [...opts.source.accessibleStatuses] : []);
|
|
194
282
|
this.imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
|
|
195
283
|
this.contentConfig = opts.content;
|
|
284
|
+
this.rendererFn = opts.renderer;
|
|
196
285
|
this.waitUntil = opts.waitUntil;
|
|
286
|
+
this.logger = mergeLoggers(opts.plugins ?? [], opts.logger);
|
|
287
|
+
this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks, this.logger);
|
|
288
|
+
this.maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;
|
|
289
|
+
this.retryConfig = {
|
|
290
|
+
...DEFAULT_RETRY_CONFIG,
|
|
291
|
+
...opts.rateLimiter ?? {}
|
|
292
|
+
};
|
|
293
|
+
this.cache = {
|
|
294
|
+
getList: this.cachedList.bind(this),
|
|
295
|
+
get: this.cachedGet.bind(this),
|
|
296
|
+
prefetchAll: this.prefetchAll.bind(this),
|
|
297
|
+
revalidate: this.revalidate.bind(this),
|
|
298
|
+
sync: this.syncFromWebhook.bind(this),
|
|
299
|
+
checkList: this.checkListUpdate.bind(this),
|
|
300
|
+
checkItem: this.checkItemUpdate.bind(this)
|
|
301
|
+
};
|
|
197
302
|
}
|
|
198
303
|
// ── コンテンツ取得 ──────────────────────────────────────────────────────
|
|
199
304
|
/** 公開済みコンテンツ一覧をソースから直接取得する。 */
|
|
200
305
|
list() {
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
306
|
+
return withRetry(
|
|
307
|
+
() => this.source.list({
|
|
308
|
+
publishedStatuses: this.publishedStatuses.length > 0 ? this.publishedStatuses : void 0
|
|
309
|
+
}),
|
|
310
|
+
{
|
|
311
|
+
...this.retryConfig,
|
|
312
|
+
onRetry: (attempt, status) => {
|
|
313
|
+
this.logger?.warn?.("list() \u30EA\u30C8\u30E9\u30A4\u4E2D", { attempt, status });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
204
317
|
}
|
|
205
318
|
/** スラッグでコンテンツをソースから直接取得する。 */
|
|
206
|
-
async
|
|
207
|
-
const item = await this.source.findBySlug(slug)
|
|
319
|
+
async find(slug) {
|
|
320
|
+
const item = await withRetry(() => this.source.findBySlug(slug), {
|
|
321
|
+
...this.retryConfig,
|
|
322
|
+
onRetry: (attempt, status) => {
|
|
323
|
+
this.logger?.warn?.("find() \u30EA\u30C8\u30E9\u30A4\u4E2D", { attempt, status, slug });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
208
326
|
if (!item) return null;
|
|
209
|
-
if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
|
|
327
|
+
if (this.accessibleStatuses.length > 0 && (!item.status || !this.accessibleStatuses.includes(item.status))) {
|
|
210
328
|
return null;
|
|
211
329
|
}
|
|
212
330
|
return item;
|
|
213
331
|
}
|
|
332
|
+
/** 複数スラッグをまとめてソースから直接取得する。accessibleStatuses フィルタを適用する。 */
|
|
333
|
+
async findMany(slugs) {
|
|
334
|
+
const results = /* @__PURE__ */ new Map();
|
|
335
|
+
await Promise.all(
|
|
336
|
+
slugs.map(async (slug) => {
|
|
337
|
+
const item = await this.find(slug);
|
|
338
|
+
if (item) results.set(slug, item);
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
214
343
|
/** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
|
|
215
344
|
isPublished(item) {
|
|
216
345
|
if (this.publishedStatuses.length === 0) return true;
|
|
217
|
-
return this.publishedStatuses.includes(item.status);
|
|
346
|
+
return !!item.status && this.publishedStatuses.includes(item.status);
|
|
218
347
|
}
|
|
219
|
-
/** コンテンツを Markdown → HTML にレンダリングし、CachedItem
|
|
348
|
+
/** コンテンツを Markdown → HTML にレンダリングし、CachedItem として返す。キャッシュには保存しない。 */
|
|
220
349
|
async render(item) {
|
|
221
350
|
return this.buildCachedItem(item);
|
|
222
351
|
}
|
|
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));
|
|
352
|
+
/** QueryBuilder を返す。ステータス・タグ・ページネーションなどを連鎖で指定できる。 */
|
|
353
|
+
query() {
|
|
354
|
+
return new QueryBuilder(this.source, this.publishedStatuses);
|
|
245
355
|
}
|
|
246
|
-
/**
|
|
247
|
-
async
|
|
356
|
+
/** 静的生成用のスラッグ一覧を返す。 */
|
|
357
|
+
async getStaticSlugs() {
|
|
248
358
|
const items = await this.list();
|
|
249
|
-
return items.
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
};
|
|
359
|
+
return items.map((item) => item.slug);
|
|
360
|
+
}
|
|
361
|
+
// ── 画像配信 ──────────────────────────────────────────────────────────
|
|
362
|
+
/** ハッシュキーでキャッシュ画像を取得する。 */
|
|
363
|
+
getCachedImage(hash) {
|
|
364
|
+
return this.imgCache.get(hash);
|
|
365
|
+
}
|
|
366
|
+
/** ハッシュキーでキャッシュ画像を Response として返す。 */
|
|
367
|
+
async createCachedImageResponse(hash) {
|
|
368
|
+
const object = await this.imgCache.get(hash);
|
|
369
|
+
if (!object) return null;
|
|
370
|
+
const headers = new Headers();
|
|
371
|
+
if (object.contentType) headers.set("content-type", object.contentType);
|
|
372
|
+
headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
373
|
+
return new Response(object.data, { headers });
|
|
264
374
|
}
|
|
265
|
-
|
|
266
|
-
async
|
|
375
|
+
// ── キャッシュ優先取得(Stale-While-Revalidate) ─────────────────────
|
|
376
|
+
async cachedList() {
|
|
377
|
+
const cached = await this.docCache.getList();
|
|
378
|
+
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
379
|
+
this.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
|
|
380
|
+
return { items: cached.items, isStale: false, cachedAt: cached.cachedAt };
|
|
381
|
+
}
|
|
382
|
+
this.hooks.onListCacheMiss?.();
|
|
267
383
|
const items = await this.list();
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
384
|
+
const cachedAt = Date.now();
|
|
385
|
+
const save = this.docCache.setList({ items, cachedAt });
|
|
386
|
+
if (this.waitUntil) {
|
|
387
|
+
this.waitUntil(save);
|
|
388
|
+
} else {
|
|
389
|
+
await save;
|
|
390
|
+
}
|
|
391
|
+
return { items, isStale: !!cached, cachedAt };
|
|
274
392
|
}
|
|
275
|
-
|
|
393
|
+
async cachedGet(slug) {
|
|
394
|
+
const cached = await this.docCache.getItem(slug);
|
|
395
|
+
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
396
|
+
this.hooks.onCacheHit?.(slug, cached);
|
|
397
|
+
return cached;
|
|
398
|
+
}
|
|
399
|
+
this.hooks.onCacheMiss?.(slug);
|
|
400
|
+
const item = await this.find(slug);
|
|
401
|
+
if (!item) return null;
|
|
402
|
+
const entry = await this.buildCachedItem(item);
|
|
403
|
+
const save = this.docCache.setItem(slug, entry);
|
|
404
|
+
if (this.waitUntil) {
|
|
405
|
+
this.waitUntil(save);
|
|
406
|
+
} else {
|
|
407
|
+
await save;
|
|
408
|
+
}
|
|
409
|
+
return entry;
|
|
410
|
+
}
|
|
411
|
+
// ── キャッシュ管理 ────────────────────────────────────────────────────
|
|
276
412
|
async prefetchAll(opts) {
|
|
277
413
|
const items = await this.list();
|
|
278
|
-
const concurrency = opts?.concurrency ??
|
|
414
|
+
const concurrency = opts?.concurrency ?? this.maxConcurrent;
|
|
279
415
|
let ok = 0;
|
|
280
416
|
let failed = 0;
|
|
281
417
|
for (let i = 0; i < items.length; i += concurrency) {
|
|
@@ -286,8 +422,16 @@ var CMS = class {
|
|
|
286
422
|
const rendered = await this.buildCachedItem(item);
|
|
287
423
|
await this.docCache.setItem(item.slug, rendered);
|
|
288
424
|
ok++;
|
|
289
|
-
} catch {
|
|
425
|
+
} catch (err) {
|
|
290
426
|
failed++;
|
|
427
|
+
this.logger?.warn?.(
|
|
428
|
+
"prefetchAll: \u30A2\u30A4\u30C6\u30E0\u306E\u4E8B\u524D\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u306B\u5931\u6557",
|
|
429
|
+
{
|
|
430
|
+
slug: item.slug,
|
|
431
|
+
pageId: item.id,
|
|
432
|
+
error: err instanceof Error ? err.message : String(err)
|
|
433
|
+
}
|
|
434
|
+
);
|
|
291
435
|
}
|
|
292
436
|
})
|
|
293
437
|
);
|
|
@@ -296,21 +440,14 @@ var CMS = class {
|
|
|
296
440
|
await this.docCache.setList({ items, cachedAt: Date.now() });
|
|
297
441
|
return { ok, failed };
|
|
298
442
|
}
|
|
299
|
-
/** 静的生成用のスラッグ一覧を返す。 */
|
|
300
|
-
async getStaticSlugs() {
|
|
301
|
-
const items = await this.list();
|
|
302
|
-
return items.map((item) => item.slug);
|
|
303
|
-
}
|
|
304
|
-
/** 指定スコープのキャッシュを無効化する。 */
|
|
305
443
|
async revalidate(scope) {
|
|
306
444
|
if (!this.docCache.invalidate) return;
|
|
307
445
|
await this.docCache.invalidate(scope ?? "all");
|
|
308
446
|
}
|
|
309
|
-
/** Webhook ペイロードを元にキャッシュを同期する。 */
|
|
310
447
|
async syncFromWebhook(payload) {
|
|
311
448
|
const updated = [];
|
|
312
449
|
if (payload?.slug) {
|
|
313
|
-
const item = await this.
|
|
450
|
+
const item = await this.find(payload.slug);
|
|
314
451
|
if (item) {
|
|
315
452
|
const rendered = await this.buildCachedItem(item);
|
|
316
453
|
await this.docCache.setItem(item.slug, rendered);
|
|
@@ -325,39 +462,6 @@ var CMS = class {
|
|
|
325
462
|
}
|
|
326
463
|
return { updated };
|
|
327
464
|
}
|
|
328
|
-
// ── キャッシュ優先取得(Stale-While-Revalidate) ─────────────────────
|
|
329
|
-
/** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
|
|
330
|
-
async getList() {
|
|
331
|
-
const cached = await this.docCache.getList();
|
|
332
|
-
if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
|
|
333
|
-
return {
|
|
334
|
-
items: cached.items,
|
|
335
|
-
listVersion: buildListVersion(cached.items)
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
const items = await this.list();
|
|
339
|
-
const save = this.docCache.setList({ items, cachedAt: Date.now() });
|
|
340
|
-
if (this.waitUntil) {
|
|
341
|
-
this.waitUntil(save);
|
|
342
|
-
} else {
|
|
343
|
-
await save;
|
|
344
|
-
}
|
|
345
|
-
return { items, listVersion: buildListVersion(items) };
|
|
346
|
-
}
|
|
347
|
-
/** キャッシュ優先で単一コンテンツを返す(SWR)。 */
|
|
348
|
-
async getItem(slug) {
|
|
349
|
-
const cached = await this.docCache.getItem(slug);
|
|
350
|
-
if (cached && !isStale(cached.cachedAt, this.ttlMs)) return cached;
|
|
351
|
-
const entry = await this.renderBySlug(slug);
|
|
352
|
-
if (!entry) return null;
|
|
353
|
-
const save = this.docCache.setItem(slug, entry);
|
|
354
|
-
if (this.waitUntil) {
|
|
355
|
-
this.waitUntil(save);
|
|
356
|
-
} else {
|
|
357
|
-
await save;
|
|
358
|
-
}
|
|
359
|
-
return entry;
|
|
360
|
-
}
|
|
361
465
|
async checkListUpdate(version) {
|
|
362
466
|
const items = await this.list();
|
|
363
467
|
const serverVersion = buildListVersion(items);
|
|
@@ -366,12 +470,11 @@ var CMS = class {
|
|
|
366
470
|
return { changed: true, items };
|
|
367
471
|
}
|
|
368
472
|
async checkItemUpdate(slug, lastEdited) {
|
|
369
|
-
const item = await this.
|
|
473
|
+
const item = await this.find(slug);
|
|
370
474
|
if (!item) return { changed: false };
|
|
371
475
|
if (!this.isPublished(item)) return { changed: false };
|
|
372
476
|
if (item.updatedAt === lastEdited) return { changed: false };
|
|
373
|
-
const entry = await this.
|
|
374
|
-
if (!entry) return { changed: false };
|
|
477
|
+
const entry = await this.buildCachedItem(item);
|
|
375
478
|
await this.docCache.setItem(slug, entry);
|
|
376
479
|
return {
|
|
377
480
|
changed: true,
|
|
@@ -380,29 +483,21 @@ var CMS = class {
|
|
|
380
483
|
notionUpdatedAt: entry.notionUpdatedAt
|
|
381
484
|
};
|
|
382
485
|
}
|
|
383
|
-
// ── 画像配信 ───────────────────────────────────────────────────────────
|
|
384
|
-
/** ハッシュキーでキャッシュ画像を取得する。 */
|
|
385
|
-
getCachedImage(hash) {
|
|
386
|
-
return this.imgCache.get(hash);
|
|
387
|
-
}
|
|
388
|
-
/** ハッシュキーでキャッシュ画像を Response として返す。 */
|
|
389
|
-
async createCachedImageResponse(hash) {
|
|
390
|
-
const object = await this.imgCache.get(hash);
|
|
391
|
-
if (!object) return null;
|
|
392
|
-
const headers = new Headers();
|
|
393
|
-
if (object.contentType) headers.set("content-type", object.contentType);
|
|
394
|
-
headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
395
|
-
return new Response(object.data, { headers });
|
|
396
|
-
}
|
|
397
486
|
// ── プライベートヘルパー ────────────────────────────────────────────────
|
|
398
487
|
async buildCachedItem(item) {
|
|
488
|
+
const start = Date.now();
|
|
489
|
+
this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u958B\u59CB", {
|
|
490
|
+
slug: item.slug,
|
|
491
|
+
pageId: item.id
|
|
492
|
+
});
|
|
493
|
+
this.hooks.onRenderStart?.(item.slug);
|
|
399
494
|
let markdown;
|
|
400
495
|
try {
|
|
401
496
|
markdown = await this.source.loadMarkdown(item);
|
|
402
497
|
} catch (err) {
|
|
403
498
|
if (isCMSError(err)) throw err;
|
|
404
499
|
throw new CMSError({
|
|
405
|
-
code: "
|
|
500
|
+
code: "source/load_markdown_failed",
|
|
406
501
|
message: "Failed to load markdown from source.",
|
|
407
502
|
cause: err,
|
|
408
503
|
context: {
|
|
@@ -414,18 +509,18 @@ var CMS = class {
|
|
|
414
509
|
}
|
|
415
510
|
const cacheImage = this.hasImageCache ? buildCacheImageFn(this.imgCache, this.imageProxyBase) : void 0;
|
|
416
511
|
let html;
|
|
512
|
+
const rendererFn = this.rendererFn ?? await loadDefaultRenderer();
|
|
417
513
|
try {
|
|
418
|
-
html = await
|
|
514
|
+
html = await rendererFn(markdown, {
|
|
419
515
|
imageProxyBase: this.imageProxyBase,
|
|
420
516
|
cacheImage,
|
|
421
517
|
remarkPlugins: this.contentConfig?.remarkPlugins,
|
|
422
|
-
rehypePlugins: this.contentConfig?.rehypePlugins
|
|
423
|
-
render: this.contentConfig?.render
|
|
518
|
+
rehypePlugins: this.contentConfig?.rehypePlugins
|
|
424
519
|
});
|
|
425
520
|
} catch (err) {
|
|
426
521
|
if (isCMSError(err)) throw err;
|
|
427
522
|
throw new CMSError({
|
|
428
|
-
code: "
|
|
523
|
+
code: "renderer/failed",
|
|
429
524
|
message: "Failed to render markdown.",
|
|
430
525
|
cause: err,
|
|
431
526
|
context: {
|
|
@@ -435,67 +530,69 @@ var CMS = class {
|
|
|
435
530
|
}
|
|
436
531
|
});
|
|
437
532
|
}
|
|
438
|
-
|
|
533
|
+
if (this.hooks.afterRender) {
|
|
534
|
+
html = await this.hooks.afterRender(html, item);
|
|
535
|
+
}
|
|
536
|
+
let result = {
|
|
439
537
|
html,
|
|
440
538
|
item,
|
|
441
539
|
notionUpdatedAt: item.updatedAt,
|
|
442
540
|
cachedAt: Date.now()
|
|
443
541
|
};
|
|
542
|
+
if (this.hooks.beforeCache) {
|
|
543
|
+
result = await this.hooks.beforeCache(result);
|
|
544
|
+
}
|
|
545
|
+
const durationMs = Date.now() - start;
|
|
546
|
+
this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u5B8C\u4E86", {
|
|
547
|
+
slug: item.slug,
|
|
548
|
+
durationMs
|
|
549
|
+
});
|
|
550
|
+
this.hooks.onRenderEnd?.(item.slug, durationMs);
|
|
551
|
+
return result;
|
|
444
552
|
}
|
|
445
553
|
};
|
|
554
|
+
async function loadDefaultRenderer() {
|
|
555
|
+
try {
|
|
556
|
+
const mod = await import("@notion-headless-cms/renderer");
|
|
557
|
+
return mod.renderMarkdown;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
throw new CMSError({
|
|
560
|
+
code: "renderer/failed",
|
|
561
|
+
message: "renderer \u30AA\u30D7\u30B7\u30E7\u30F3\u304C\u672A\u6307\u5B9A\u3067 @notion-headless-cms/renderer \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002 createCMS({ renderer }) \u3067\u30EC\u30F3\u30C0\u30E9\u30FC\u3092\u6CE8\u5165\u3059\u308B\u304B\u3001@notion-headless-cms/renderer \u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
562
|
+
cause: err,
|
|
563
|
+
context: { operation: "loadDefaultRenderer" }
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function buildListVersion(items) {
|
|
568
|
+
return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
|
|
569
|
+
}
|
|
446
570
|
function createCMS(opts) {
|
|
447
571
|
return new CMS(opts);
|
|
448
572
|
}
|
|
449
573
|
|
|
450
|
-
// src/
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
id: z.string().min(1),
|
|
454
|
-
slug: z.string(),
|
|
455
|
-
status: z.string(),
|
|
456
|
-
publishedAt: z.string().min(1),
|
|
457
|
-
updatedAt: z.string().min(1)
|
|
458
|
-
});
|
|
459
|
-
function getPlainText(items) {
|
|
460
|
-
return items?.map((item) => item.plain_text).join("") ?? "";
|
|
461
|
-
}
|
|
462
|
-
function mapItem(page, props) {
|
|
463
|
-
const statusProperty = page.properties[props.status];
|
|
464
|
-
const dateProperty = page.properties[props.date];
|
|
465
|
-
const parsed = baseContentItemSchema.safeParse({
|
|
466
|
-
id: page.id,
|
|
467
|
-
slug: getPlainText(
|
|
468
|
-
page.properties[props.slug]?.rich_text
|
|
469
|
-
),
|
|
470
|
-
status: statusProperty?.status?.name ?? statusProperty?.select?.name ?? "",
|
|
471
|
-
publishedAt: dateProperty?.date?.start ?? page.created_time,
|
|
472
|
-
updatedAt: page.last_edited_time
|
|
473
|
-
});
|
|
474
|
-
if (!parsed.success) {
|
|
475
|
-
throw new CMSError({
|
|
476
|
-
code: "NOTION_ITEM_SCHEMA_INVALID",
|
|
477
|
-
message: "Failed to parse Notion page into BaseContentItem.",
|
|
478
|
-
context: {
|
|
479
|
-
operation: "mapItem",
|
|
480
|
-
pageId: page.id,
|
|
481
|
-
issues: JSON.stringify(parsed.error.issues)
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
return parsed.data;
|
|
574
|
+
// src/types/plugin.ts
|
|
575
|
+
function definePlugin(plugin) {
|
|
576
|
+
return plugin;
|
|
486
577
|
}
|
|
487
578
|
export {
|
|
488
579
|
CMS,
|
|
489
580
|
CMSError,
|
|
581
|
+
DEFAULT_RETRY_CONFIG,
|
|
582
|
+
QueryBuilder,
|
|
490
583
|
createCMS,
|
|
491
|
-
|
|
584
|
+
definePlugin,
|
|
492
585
|
isCMSError,
|
|
586
|
+
isCMSErrorInNamespace,
|
|
493
587
|
isStale,
|
|
494
|
-
mapItem,
|
|
495
588
|
memoryCache,
|
|
496
589
|
memoryDocumentCache,
|
|
497
590
|
memoryImageCache,
|
|
591
|
+
mergeHooks,
|
|
592
|
+
mergeLoggers,
|
|
498
593
|
noopDocumentCache,
|
|
499
594
|
noopImageCache,
|
|
500
|
-
sha256Hex
|
|
595
|
+
sha256Hex,
|
|
596
|
+
withRetry
|
|
501
597
|
};
|
|
598
|
+
//# sourceMappingURL=index.js.map
|