@notionx/core 0.1.0
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/admin/index.d.ts +137 -0
- package/dist/admin/index.js +206 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/index.d.ts +324 -0
- package/dist/admin/pages/index.js +827 -0
- package/dist/admin/pages/index.js.map +1 -0
- package/dist/auth/auth-pages/forgot-password.d.ts +20 -0
- package/dist/auth/auth-pages/forgot-password.js +70 -0
- package/dist/auth/auth-pages/forgot-password.js.map +1 -0
- package/dist/auth/auth-pages/index.d.ts +6 -0
- package/dist/auth/auth-pages/index.js +342 -0
- package/dist/auth/auth-pages/index.js.map +1 -0
- package/dist/auth/auth-pages/login.d.ts +30 -0
- package/dist/auth/auth-pages/login.js +125 -0
- package/dist/auth/auth-pages/login.js.map +1 -0
- package/dist/auth/auth-pages/register.d.ts +17 -0
- package/dist/auth/auth-pages/register.js +81 -0
- package/dist/auth/auth-pages/register.js.map +1 -0
- package/dist/auth/auth-pages/reset-password.d.ts +18 -0
- package/dist/auth/auth-pages/reset-password.js +72 -0
- package/dist/auth/auth-pages/reset-password.js.map +1 -0
- package/dist/auth/index.d.ts +72 -0
- package/dist/auth/index.js +1011 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/passwords.d.ts +6 -0
- package/dist/auth/passwords.js +79 -0
- package/dist/auth/passwords.js.map +1 -0
- package/dist/auth/rate-limit.d.ts +28 -0
- package/dist/auth/rate-limit.js +245 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth/routes/google-callback.d.ts +6 -0
- package/dist/auth/routes/google-callback.js +404 -0
- package/dist/auth/routes/google-callback.js.map +1 -0
- package/dist/auth/routes/google.d.ts +6 -0
- package/dist/auth/routes/google.js +250 -0
- package/dist/auth/routes/google.js.map +1 -0
- package/dist/auth/routes/index.d.ts +22 -0
- package/dist/auth/routes/index.js +619 -0
- package/dist/auth/routes/index.js.map +1 -0
- package/dist/auth/routes/verify-email.d.ts +6 -0
- package/dist/auth/routes/verify-email.js +317 -0
- package/dist/auth/routes/verify-email.js.map +1 -0
- package/dist/auth/routes/viewer.d.ts +6 -0
- package/dist/auth/routes/viewer.js +372 -0
- package/dist/auth/routes/viewer.js.map +1 -0
- package/dist/auth/session.d.ts +9 -0
- package/dist/auth/session.js +1 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/turnstile.d.ts +20 -0
- package/dist/auth/turnstile.js +301 -0
- package/dist/auth/turnstile.js.map +1 -0
- package/dist/auth/user-session.d.ts +42 -0
- package/dist/auth/user-session.js +419 -0
- package/dist/auth/user-session.js.map +1 -0
- package/dist/auth/users.d.ts +112 -0
- package/dist/auth/users.js +558 -0
- package/dist/auth/users.js.map +1 -0
- package/dist/bootstrap-CN2g76M6.d.ts +67 -0
- package/dist/cache/index.d.ts +6 -0
- package/dist/cache/index.js +47 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/content/admin-summary.d.ts +24 -0
- package/dist/content/admin-summary.js +36 -0
- package/dist/content/admin-summary.js.map +1 -0
- package/dist/content/index.d.ts +9 -0
- package/dist/content/index.js +473 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/models.d.ts +69 -0
- package/dist/content/models.js +24 -0
- package/dist/content/models.js.map +1 -0
- package/dist/content/prewarm.d.ts +28 -0
- package/dist/content/prewarm.js +56 -0
- package/dist/content/prewarm.js.map +1 -0
- package/dist/content/revalidate.d.ts +37 -0
- package/dist/content/revalidate.js +170 -0
- package/dist/content/revalidate.js.map +1 -0
- package/dist/content/search-index.d.ts +54 -0
- package/dist/content/search-index.js +172 -0
- package/dist/content/search-index.js.map +1 -0
- package/dist/content/search.d.ts +8 -0
- package/dist/content/search.js +57 -0
- package/dist/content/search.js.map +1 -0
- package/dist/doctor/cli.d.ts +1 -0
- package/dist/doctor/cli.js +360 -0
- package/dist/doctor/cli.js.map +1 -0
- package/dist/doctor/index.d.ts +139 -0
- package/dist/doctor/index.js +289 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/email/index.d.ts +38 -0
- package/dist/email/index.js +126 -0
- package/dist/email/index.js.map +1 -0
- package/dist/env-C5qu-0R-.d.ts +35 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/i18n/index.d.ts +26 -0
- package/dist/i18n/index.js +73 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1281 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/admin/index.d.ts +75 -0
- package/dist/internal/admin/index.js +365 -0
- package/dist/internal/admin/index.js.map +1 -0
- package/dist/media/index.d.ts +24 -0
- package/dist/media/index.js +86 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/routes/index.d.ts +1 -0
- package/dist/media/routes/index.js +585 -0
- package/dist/media/routes/index.js.map +1 -0
- package/dist/media/routes/notion-media.d.ts +19 -0
- package/dist/media/routes/notion-media.js +588 -0
- package/dist/media/routes/notion-media.js.map +1 -0
- package/dist/middleware.d.ts +95 -0
- package/dist/middleware.js +79 -0
- package/dist/middleware.js.map +1 -0
- package/dist/notion/block-text.d.ts +5 -0
- package/dist/notion/block-text.js +37 -0
- package/dist/notion/block-text.js.map +1 -0
- package/dist/notion/blocks.d.ts +24 -0
- package/dist/notion/blocks.js +46 -0
- package/dist/notion/blocks.js.map +1 -0
- package/dist/notion/client.d.ts +7 -0
- package/dist/notion/client.js +13 -0
- package/dist/notion/client.js.map +1 -0
- package/dist/notion/config.d.ts +25 -0
- package/dist/notion/config.js +147 -0
- package/dist/notion/config.js.map +1 -0
- package/dist/notion/content-cache.d.ts +45 -0
- package/dist/notion/content-cache.js +166 -0
- package/dist/notion/content-cache.js.map +1 -0
- package/dist/notion/generic-source.d.ts +61 -0
- package/dist/notion/generic-source.js +408 -0
- package/dist/notion/generic-source.js.map +1 -0
- package/dist/notion/index.d.ts +13 -0
- package/dist/notion/index.js +1278 -0
- package/dist/notion/index.js.map +1 -0
- package/dist/notion/mappers.d.ts +1 -0
- package/dist/notion/mappers.js +152 -0
- package/dist/notion/mappers.js.map +1 -0
- package/dist/notion/media.d.ts +22 -0
- package/dist/notion/media.js +209 -0
- package/dist/notion/media.js.map +1 -0
- package/dist/notion/property-mappers.d.ts +24 -0
- package/dist/notion/property-mappers.js +152 -0
- package/dist/notion/property-mappers.js.map +1 -0
- package/dist/notion/routes/index.d.ts +8 -0
- package/dist/notion/routes/index.js +428 -0
- package/dist/notion/routes/index.js.map +1 -0
- package/dist/notion/routes/webhook.d.ts +98 -0
- package/dist/notion/routes/webhook.js +428 -0
- package/dist/notion/routes/webhook.js.map +1 -0
- package/dist/notion/types.d.ts +152 -0
- package/dist/notion/types.js +1 -0
- package/dist/notion/types.js.map +1 -0
- package/dist/notion/webhook.d.ts +83 -0
- package/dist/notion/webhook.js +490 -0
- package/dist/notion/webhook.js.map +1 -0
- package/dist/platform/capabilities.d.ts +34 -0
- package/dist/platform/capabilities.js +42 -0
- package/dist/platform/capabilities.js.map +1 -0
- package/dist/platform/current.d.ts +13 -0
- package/dist/platform/current.js +181 -0
- package/dist/platform/current.js.map +1 -0
- package/dist/platform/index.d.ts +5 -0
- package/dist/platform/index.js +269 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/runtime.d.ts +118 -0
- package/dist/platform/runtime.js +160 -0
- package/dist/platform/runtime.js.map +1 -0
- package/dist/platform/selection.d.ts +10 -0
- package/dist/platform/selection.js +22 -0
- package/dist/platform/selection.js.map +1 -0
- package/dist/storage/index.d.ts +17 -0
- package/dist/storage/index.js +218 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/routes/cdn.d.ts +19 -0
- package/dist/storage/routes/cdn.js +289 -0
- package/dist/storage/routes/cdn.js.map +1 -0
- package/dist/storage/routes/files.d.ts +27 -0
- package/dist/storage/routes/files.js +216 -0
- package/dist/storage/routes/files.js.map +1 -0
- package/dist/storage/routes/index.d.ts +2 -0
- package/dist/storage/routes/index.js +352 -0
- package/dist/storage/routes/index.js.map +1 -0
- package/dist/types-BsAcZSNX.d.ts +94 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/util/index.d.ts +18 -0
- package/dist/util/index.js +48 -0
- package/dist/util/index.js.map +1 -0
- package/dist/worker/index.d.ts +6 -0
- package/dist/worker/index.js +1026 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/routes/content-prewarm.d.ts +34 -0
- package/dist/worker/routes/content-prewarm.js +38 -0
- package/dist/worker/routes/content-prewarm.js.map +1 -0
- package/dist/worker/routes/content-revalidate.d.ts +81 -0
- package/dist/worker/routes/content-revalidate.js +64 -0
- package/dist/worker/routes/content-revalidate.js.map +1 -0
- package/dist/worker/routes/health.d.ts +14 -0
- package/dist/worker/routes/health.js +278 -0
- package/dist/worker/routes/health.js.map +1 -0
- package/dist/worker/routes/index.d.ts +6 -0
- package/dist/worker/routes/index.js +373 -0
- package/dist/worker/routes/index.js.map +1 -0
- package/package.json +124 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/content/search.ts
|
|
2
|
+
function normalizeSearchQuery(query) {
|
|
3
|
+
return String(query ?? "").normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
function searchTerms(query) {
|
|
6
|
+
const normalized = normalizeSearchQuery(query);
|
|
7
|
+
return normalized ? normalized.split(" ") : [];
|
|
8
|
+
}
|
|
9
|
+
function searchableText(values) {
|
|
10
|
+
return values.flatMap((value) => Array.isArray(value) ? value : [value]).filter(
|
|
11
|
+
(value) => ["string", "number", "boolean"].includes(typeof value)
|
|
12
|
+
).map((value) => String(value)).join(" ").normalize("NFKC").toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function matchesSearchQuery(values, query) {
|
|
15
|
+
const terms = searchTerms(query);
|
|
16
|
+
if (terms.length === 0) return true;
|
|
17
|
+
const haystack = searchableText(values);
|
|
18
|
+
return terms.every((term) => haystack.includes(term));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/content/search-index.ts
|
|
22
|
+
function indexValuesForItem(item) {
|
|
23
|
+
return [
|
|
24
|
+
item.title,
|
|
25
|
+
item.description,
|
|
26
|
+
item.author,
|
|
27
|
+
item.tags,
|
|
28
|
+
item.slug,
|
|
29
|
+
item.date,
|
|
30
|
+
item.summary,
|
|
31
|
+
item.director,
|
|
32
|
+
item.actors,
|
|
33
|
+
item.genres,
|
|
34
|
+
item.routeId,
|
|
35
|
+
item.releaseDate
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function routeIdForItem(item) {
|
|
39
|
+
return item.routeId ?? item.slug ?? "";
|
|
40
|
+
}
|
|
41
|
+
function routeOrder(items) {
|
|
42
|
+
const order = /* @__PURE__ */ new Map();
|
|
43
|
+
items.forEach((item, index) => {
|
|
44
|
+
const routeId = routeIdForItem(item);
|
|
45
|
+
if (routeId) order.set(routeId, index);
|
|
46
|
+
});
|
|
47
|
+
return order;
|
|
48
|
+
}
|
|
49
|
+
function uniqueValues(values) {
|
|
50
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
51
|
+
}
|
|
52
|
+
async function upsertSearchIndexDocument(db, document) {
|
|
53
|
+
const normalizedText = [
|
|
54
|
+
document.title,
|
|
55
|
+
document.summary,
|
|
56
|
+
document.bodyText,
|
|
57
|
+
...document.facets
|
|
58
|
+
].join(" ").normalize("NFKC").replace(/\s+/g, " ").toLowerCase();
|
|
59
|
+
await db.prepare(
|
|
60
|
+
`INSERT INTO content_search_index (
|
|
61
|
+
model_id,
|
|
62
|
+
page_id,
|
|
63
|
+
route_id,
|
|
64
|
+
title,
|
|
65
|
+
summary,
|
|
66
|
+
body_text,
|
|
67
|
+
facets,
|
|
68
|
+
normalized_text,
|
|
69
|
+
source_updated_at,
|
|
70
|
+
indexed_at
|
|
71
|
+
)
|
|
72
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
73
|
+
ON CONFLICT(model_id, route_id) DO UPDATE SET
|
|
74
|
+
page_id = excluded.page_id,
|
|
75
|
+
title = excluded.title,
|
|
76
|
+
summary = excluded.summary,
|
|
77
|
+
body_text = excluded.body_text,
|
|
78
|
+
facets = excluded.facets,
|
|
79
|
+
normalized_text = excluded.normalized_text,
|
|
80
|
+
source_updated_at = excluded.source_updated_at,
|
|
81
|
+
indexed_at = excluded.indexed_at`
|
|
82
|
+
).bind(
|
|
83
|
+
document.modelId,
|
|
84
|
+
document.pageId,
|
|
85
|
+
document.routeId,
|
|
86
|
+
document.title,
|
|
87
|
+
document.summary,
|
|
88
|
+
document.bodyText,
|
|
89
|
+
JSON.stringify(uniqueValues([...document.facets])),
|
|
90
|
+
normalizedText,
|
|
91
|
+
document.sourceUpdatedAt ?? null
|
|
92
|
+
).run();
|
|
93
|
+
}
|
|
94
|
+
async function deleteSearchIndexDocument(db, input) {
|
|
95
|
+
await db.prepare(
|
|
96
|
+
"DELETE FROM content_search_index WHERE model_id = ? AND route_id = ?"
|
|
97
|
+
).bind(input.modelId, input.routeId).run();
|
|
98
|
+
}
|
|
99
|
+
async function deleteSearchIndexForModel(db, input) {
|
|
100
|
+
await db.prepare("DELETE FROM content_search_index WHERE model_id = ?").bind(input.modelId).run();
|
|
101
|
+
}
|
|
102
|
+
async function getMissingSearchIndexRouteIds(db, input) {
|
|
103
|
+
const routeIds = uniqueValues([...input.routeIds]);
|
|
104
|
+
if (routeIds.length === 0) return [];
|
|
105
|
+
const placeholders = routeIds.map(() => "?").join(", ");
|
|
106
|
+
const result = await db.prepare(
|
|
107
|
+
`SELECT route_id
|
|
108
|
+
FROM content_search_index
|
|
109
|
+
WHERE model_id = ? AND route_id IN (${placeholders})`
|
|
110
|
+
).bind(input.modelId, ...routeIds).all();
|
|
111
|
+
const present = new Set((result.results ?? []).map((row) => row.route_id));
|
|
112
|
+
return routeIds.filter((routeId) => !present.has(routeId));
|
|
113
|
+
}
|
|
114
|
+
async function querySearchIndexRouteIds(db, input) {
|
|
115
|
+
const query = normalizeSearchQuery(input.query);
|
|
116
|
+
if (!query) return [];
|
|
117
|
+
const terms = query.split(" ").map((term) => `%${term}%`);
|
|
118
|
+
const clauses = terms.map(
|
|
119
|
+
() => "(normalized_text LIKE ? OR title LIKE ? OR summary LIKE ? OR facets LIKE ?)"
|
|
120
|
+
).join(" AND ");
|
|
121
|
+
const values = terms.flatMap((term) => [term, term, term, term]);
|
|
122
|
+
const result = await db.prepare(
|
|
123
|
+
`SELECT route_id
|
|
124
|
+
FROM content_search_index
|
|
125
|
+
WHERE model_id = ? AND ${clauses}
|
|
126
|
+
ORDER BY indexed_at DESC
|
|
127
|
+
LIMIT ?`
|
|
128
|
+
).bind(input.modelId, ...values, input.limit ?? 200).all();
|
|
129
|
+
return (result.results ?? []).map((row) => row.route_id).filter((routeId) => typeof routeId === "string");
|
|
130
|
+
}
|
|
131
|
+
async function filterItemsBySearchIndex(items, query, input) {
|
|
132
|
+
const normalized = normalizeSearchQuery(query);
|
|
133
|
+
if (!normalized) return [...items];
|
|
134
|
+
if (!input.getDatabase) return input.filterFallback(items, normalized);
|
|
135
|
+
try {
|
|
136
|
+
const db = await input.getDatabase();
|
|
137
|
+
if (!db) return input.filterFallback(items, normalized);
|
|
138
|
+
const routeIds = await querySearchIndexRouteIds(db, {
|
|
139
|
+
modelId: input.modelId,
|
|
140
|
+
query: normalized,
|
|
141
|
+
limit: Math.max(items.length, 200)
|
|
142
|
+
});
|
|
143
|
+
if (routeIds.length === 0) return input.filterFallback(items, normalized);
|
|
144
|
+
const order = routeOrder(items);
|
|
145
|
+
const matched = new Set(routeIds);
|
|
146
|
+
return items.filter((item) => matched.has(routeIdForItem(item))).sort(
|
|
147
|
+
(a, b) => (order.get(routeIdForItem(a)) ?? 0) - (order.get(routeIdForItem(b)) ?? 0)
|
|
148
|
+
);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
tag: "content_search_index_error",
|
|
153
|
+
modelId: input.modelId,
|
|
154
|
+
message: error instanceof Error ? error.message : String(error)
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
return input.filterFallback(items, normalized);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function matchesIndexedItem(item, query) {
|
|
161
|
+
return matchesSearchQuery(indexValuesForItem(item), query);
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
deleteSearchIndexDocument,
|
|
165
|
+
deleteSearchIndexForModel,
|
|
166
|
+
filterItemsBySearchIndex,
|
|
167
|
+
getMissingSearchIndexRouteIds,
|
|
168
|
+
matchesIndexedItem,
|
|
169
|
+
querySearchIndexRouteIds,
|
|
170
|
+
upsertSearchIndexDocument
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=search-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/content/search.ts","../../src/content/search-index.ts"],"sourcesContent":["// packages/nextion/src/content/search.ts\n//\n// Generic text-search helpers for content sources.\n\nimport type {\n NotionMovieListItem,\n NotionPostListItem,\n} from \"../notion/types\";\n\nexport function normalizeSearchQuery(query: string | null | undefined) {\n return String(query ?? \"\")\n .normalize(\"NFKC\")\n .trim()\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n}\n\nfunction searchTerms(query: string | null | undefined) {\n const normalized = normalizeSearchQuery(query);\n return normalized ? normalized.split(\" \") : [];\n}\n\nfunction searchableText(values: readonly unknown[]) {\n return values\n .flatMap((value) => (Array.isArray(value) ? value : [value]))\n .filter((value): value is string | number | boolean =>\n [\"string\", \"number\", \"boolean\"].includes(typeof value)\n )\n .map((value) => String(value))\n .join(\" \")\n .normalize(\"NFKC\")\n .toLowerCase();\n}\n\nexport function matchesSearchQuery(\n values: readonly unknown[],\n query: string | null | undefined\n) {\n const terms = searchTerms(query);\n if (terms.length === 0) return true;\n\n const haystack = searchableText(values);\n return terms.every((term) => haystack.includes(term));\n}\n\nexport function filterPostsBySearch<TPost extends NotionPostListItem>(\n posts: readonly TPost[],\n query: string | null | undefined\n) {\n return posts.filter((post) =>\n matchesSearchQuery(\n [\n post.title,\n post.description,\n post.author,\n post.tags,\n post.slug,\n post.date,\n ],\n query\n )\n );\n}\n\nexport function filterMoviesBySearch<TMovie extends NotionMovieListItem>(\n movies: readonly TMovie[],\n query: string | null | undefined\n) {\n return movies.filter((movie) =>\n matchesSearchQuery(\n [\n movie.title,\n movie.summary,\n movie.director,\n movie.actors,\n movie.genres,\n movie.releaseDate,\n movie.routeId,\n ],\n query\n )\n );\n}\n","// packages/nextion/src/content/search-index.ts\n//\n// Generic D1-backed content search index helpers.\n\nimport type { SqlDatabaseAdapter } from \"../platform/runtime\";\nimport { matchesSearchQuery, normalizeSearchQuery } from \"./search\";\n\nexport type SearchIndexedItem = {\n pageId?: string;\n slug?: string;\n routeId?: string;\n title: string;\n description?: string;\n date?: string;\n author?: string;\n tags?: readonly string[];\n releaseDate?: string;\n director?: string;\n actors?: string;\n summary?: string;\n genres?: readonly string[];\n};\n\nexport type SearchIndexDocument = {\n modelId: string;\n pageId: string;\n routeId: string;\n title: string;\n summary: string;\n bodyText: string;\n facets: readonly string[];\n sourceUpdatedAt?: string | null;\n};\n\ntype SearchIndexRow = {\n route_id: string;\n};\n\ntype MaybePromise<T> = T | Promise<T>;\n\nfunction indexValuesForItem(item: SearchIndexedItem) {\n return [\n item.title,\n item.description,\n item.author,\n item.tags,\n item.slug,\n item.date,\n item.summary,\n item.director,\n item.actors,\n item.genres,\n item.routeId,\n item.releaseDate,\n ];\n}\n\nfunction routeIdForItem(item: SearchIndexedItem) {\n return item.routeId ?? item.slug ?? \"\";\n}\n\nfunction routeOrder<T extends SearchIndexedItem>(items: readonly T[]) {\n const order = new Map<string, number>();\n items.forEach((item, index) => {\n const routeId = routeIdForItem(item);\n if (routeId) order.set(routeId, index);\n });\n return order;\n}\n\nfunction uniqueValues(values: readonly string[]) {\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));\n}\n\nexport async function upsertSearchIndexDocument(\n db: SqlDatabaseAdapter,\n document: SearchIndexDocument\n) {\n const normalizedText = [\n document.title,\n document.summary,\n document.bodyText,\n ...document.facets,\n ]\n .join(\" \")\n .normalize(\"NFKC\")\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n\n await db\n .prepare(\n `INSERT INTO content_search_index (\n model_id,\n page_id,\n route_id,\n title,\n summary,\n body_text,\n facets,\n normalized_text,\n source_updated_at,\n indexed_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))\n ON CONFLICT(model_id, route_id) DO UPDATE SET\n page_id = excluded.page_id,\n title = excluded.title,\n summary = excluded.summary,\n body_text = excluded.body_text,\n facets = excluded.facets,\n normalized_text = excluded.normalized_text,\n source_updated_at = excluded.source_updated_at,\n indexed_at = excluded.indexed_at`\n )\n .bind(\n document.modelId,\n document.pageId,\n document.routeId,\n document.title,\n document.summary,\n document.bodyText,\n JSON.stringify(uniqueValues([...document.facets])),\n normalizedText,\n document.sourceUpdatedAt ?? null\n )\n .run();\n}\n\nexport async function deleteSearchIndexDocument(\n db: SqlDatabaseAdapter,\n input: { modelId: string; routeId: string }\n) {\n await db\n .prepare(\n \"DELETE FROM content_search_index WHERE model_id = ? AND route_id = ?\"\n )\n .bind(input.modelId, input.routeId)\n .run();\n}\n\nexport async function deleteSearchIndexForModel(\n db: SqlDatabaseAdapter,\n input: { modelId: string }\n) {\n await db\n .prepare(\"DELETE FROM content_search_index WHERE model_id = ?\")\n .bind(input.modelId)\n .run();\n}\n\nexport async function getMissingSearchIndexRouteIds(\n db: SqlDatabaseAdapter,\n input: { modelId: string; routeIds: readonly string[] }\n) {\n const routeIds = uniqueValues([...input.routeIds]);\n if (routeIds.length === 0) return [];\n\n const placeholders = routeIds.map(() => \"?\").join(\", \");\n const result = await db\n .prepare(\n `SELECT route_id\n FROM content_search_index\n WHERE model_id = ? AND route_id IN (${placeholders})`\n )\n .bind(input.modelId, ...routeIds)\n .all<SearchIndexRow>();\n\n const present = new Set((result.results ?? []).map((row) => row.route_id));\n return routeIds.filter((routeId) => !present.has(routeId));\n}\n\nexport async function querySearchIndexRouteIds(\n db: SqlDatabaseAdapter,\n input: { modelId: string; query: string; limit?: number }\n) {\n const query = normalizeSearchQuery(input.query);\n if (!query) return [];\n\n const terms = query\n .split(\" \")\n .map((term) => `%${term}%`);\n const clauses = terms\n .map(\n () =>\n \"(normalized_text LIKE ? OR title LIKE ? OR summary LIKE ? OR facets LIKE ?)\"\n )\n .join(\" AND \");\n const values = terms.flatMap((term) => [term, term, term, term]);\n\n const result = await db\n .prepare(\n `SELECT route_id\n FROM content_search_index\n WHERE model_id = ? AND ${clauses}\n ORDER BY indexed_at DESC\n LIMIT ?`\n )\n .bind(input.modelId, ...values, input.limit ?? 200)\n .all<SearchIndexRow>();\n\n return (result.results ?? [])\n .map((row) => row.route_id)\n .filter((routeId): routeId is string => typeof routeId === \"string\");\n}\n\nexport async function filterItemsBySearchIndex<T extends SearchIndexedItem>(\n items: readonly T[],\n query: string | null | undefined,\n input: {\n modelId: string;\n filterFallback: (items: readonly T[], query: string | null | undefined) => T[];\n getDatabase?: () => MaybePromise<SqlDatabaseAdapter | null>;\n }\n) {\n const normalized = normalizeSearchQuery(query);\n if (!normalized) return [...items];\n if (!input.getDatabase) return input.filterFallback(items, normalized);\n\n try {\n const db = await input.getDatabase();\n if (!db) return input.filterFallback(items, normalized);\n const routeIds = await querySearchIndexRouteIds(db, {\n modelId: input.modelId,\n query: normalized,\n limit: Math.max(items.length, 200),\n });\n if (routeIds.length === 0) return input.filterFallback(items, normalized);\n\n const order = routeOrder(items);\n const matched = new Set(routeIds);\n return items\n .filter((item) => matched.has(routeIdForItem(item)))\n .sort(\n (a, b) =>\n (order.get(routeIdForItem(a)) ?? 0) -\n (order.get(routeIdForItem(b)) ?? 0)\n );\n } catch (error) {\n console.warn(\n JSON.stringify({\n tag: \"content_search_index_error\",\n modelId: input.modelId,\n message: error instanceof Error ? error.message : String(error),\n })\n );\n return input.filterFallback(items, normalized);\n }\n}\n\nexport function matchesIndexedItem(\n item: SearchIndexedItem,\n query: string | null | undefined\n) {\n return matchesSearchQuery(indexValuesForItem(item), query);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NotionMovieListItem, NotionPostListItem } from '../notion/types.js';
|
|
2
|
+
|
|
3
|
+
declare function normalizeSearchQuery(query: string | null | undefined): string;
|
|
4
|
+
declare function matchesSearchQuery(values: readonly unknown[], query: string | null | undefined): boolean;
|
|
5
|
+
declare function filterPostsBySearch<TPost extends NotionPostListItem>(posts: readonly TPost[], query: string | null | undefined): TPost[];
|
|
6
|
+
declare function filterMoviesBySearch<TMovie extends NotionMovieListItem>(movies: readonly TMovie[], query: string | null | undefined): TMovie[];
|
|
7
|
+
|
|
8
|
+
export { filterMoviesBySearch, filterPostsBySearch, matchesSearchQuery, normalizeSearchQuery };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/content/search.ts
|
|
2
|
+
function normalizeSearchQuery(query) {
|
|
3
|
+
return String(query ?? "").normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
function searchTerms(query) {
|
|
6
|
+
const normalized = normalizeSearchQuery(query);
|
|
7
|
+
return normalized ? normalized.split(" ") : [];
|
|
8
|
+
}
|
|
9
|
+
function searchableText(values) {
|
|
10
|
+
return values.flatMap((value) => Array.isArray(value) ? value : [value]).filter(
|
|
11
|
+
(value) => ["string", "number", "boolean"].includes(typeof value)
|
|
12
|
+
).map((value) => String(value)).join(" ").normalize("NFKC").toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function matchesSearchQuery(values, query) {
|
|
15
|
+
const terms = searchTerms(query);
|
|
16
|
+
if (terms.length === 0) return true;
|
|
17
|
+
const haystack = searchableText(values);
|
|
18
|
+
return terms.every((term) => haystack.includes(term));
|
|
19
|
+
}
|
|
20
|
+
function filterPostsBySearch(posts, query) {
|
|
21
|
+
return posts.filter(
|
|
22
|
+
(post) => matchesSearchQuery(
|
|
23
|
+
[
|
|
24
|
+
post.title,
|
|
25
|
+
post.description,
|
|
26
|
+
post.author,
|
|
27
|
+
post.tags,
|
|
28
|
+
post.slug,
|
|
29
|
+
post.date
|
|
30
|
+
],
|
|
31
|
+
query
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function filterMoviesBySearch(movies, query) {
|
|
36
|
+
return movies.filter(
|
|
37
|
+
(movie) => matchesSearchQuery(
|
|
38
|
+
[
|
|
39
|
+
movie.title,
|
|
40
|
+
movie.summary,
|
|
41
|
+
movie.director,
|
|
42
|
+
movie.actors,
|
|
43
|
+
movie.genres,
|
|
44
|
+
movie.releaseDate,
|
|
45
|
+
movie.routeId
|
|
46
|
+
],
|
|
47
|
+
query
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
filterMoviesBySearch,
|
|
53
|
+
filterPostsBySearch,
|
|
54
|
+
matchesSearchQuery,
|
|
55
|
+
normalizeSearchQuery
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/content/search.ts"],"sourcesContent":["// packages/nextion/src/content/search.ts\n//\n// Generic text-search helpers for content sources.\n\nimport type {\n NotionMovieListItem,\n NotionPostListItem,\n} from \"../notion/types\";\n\nexport function normalizeSearchQuery(query: string | null | undefined) {\n return String(query ?? \"\")\n .normalize(\"NFKC\")\n .trim()\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n}\n\nfunction searchTerms(query: string | null | undefined) {\n const normalized = normalizeSearchQuery(query);\n return normalized ? normalized.split(\" \") : [];\n}\n\nfunction searchableText(values: readonly unknown[]) {\n return values\n .flatMap((value) => (Array.isArray(value) ? value : [value]))\n .filter((value): value is string | number | boolean =>\n [\"string\", \"number\", \"boolean\"].includes(typeof value)\n )\n .map((value) => String(value))\n .join(\" \")\n .normalize(\"NFKC\")\n .toLowerCase();\n}\n\nexport function matchesSearchQuery(\n values: readonly unknown[],\n query: string | null | undefined\n) {\n const terms = searchTerms(query);\n if (terms.length === 0) return true;\n\n const haystack = searchableText(values);\n return terms.every((term) => haystack.includes(term));\n}\n\nexport function filterPostsBySearch<TPost extends NotionPostListItem>(\n posts: readonly TPost[],\n query: string | null | undefined\n) {\n return posts.filter((post) =>\n matchesSearchQuery(\n [\n post.title,\n post.description,\n post.author,\n post.tags,\n post.slug,\n post.date,\n ],\n query\n )\n );\n}\n\nexport function filterMoviesBySearch<TMovie extends NotionMovieListItem>(\n movies: readonly TMovie[],\n query: string | null | undefined\n) {\n return movies.filter((movie) =>\n matchesSearchQuery(\n [\n movie.title,\n movie.summary,\n movie.director,\n movie.actors,\n movie.genres,\n movie.releaseDate,\n movie.routeId,\n ],\n query\n )\n );\n}\n"],"mappings":";AASO,SAAS,qBAAqB,OAAkC;AACrE,SAAO,OAAO,SAAS,EAAE,EACtB,UAAU,MAAM,EAChB,KAAK,EACL,QAAQ,QAAQ,GAAG,EACnB,YAAY;AACjB;AAEA,SAAS,YAAY,OAAkC;AACrD,QAAM,aAAa,qBAAqB,KAAK;AAC7C,SAAO,aAAa,WAAW,MAAM,GAAG,IAAI,CAAC;AAC/C;AAEA,SAAS,eAAe,QAA4B;AAClD,SAAO,OACJ,QAAQ,CAAC,UAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAE,EAC3D;AAAA,IAAO,CAAC,UACP,CAAC,UAAU,UAAU,SAAS,EAAE,SAAS,OAAO,KAAK;AAAA,EACvD,EACC,IAAI,CAAC,UAAU,OAAO,KAAK,CAAC,EAC5B,KAAK,GAAG,EACR,UAAU,MAAM,EAChB,YAAY;AACjB;AAEO,SAAS,mBACd,QACA,OACA;AACA,QAAM,QAAQ,YAAY,KAAK;AAC/B,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,WAAW,eAAe,MAAM;AACtC,SAAO,MAAM,MAAM,CAAC,SAAS,SAAS,SAAS,IAAI,CAAC;AACtD;AAEO,SAAS,oBACd,OACA,OACA;AACA,SAAO,MAAM;AAAA,IAAO,CAAC,SACnB;AAAA,MACE;AAAA,QACE,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,MACP;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,qBACd,QACA,OACA;AACA,SAAO,OAAO;AAAA,IAAO,CAAC,UACpB;AAAA,MACE;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/doctor/cli.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// src/platform/capabilities.ts
|
|
9
|
+
var cloudflareWorkersAdapter = {
|
|
10
|
+
id: "cloudflare-workers",
|
|
11
|
+
label: "Cloudflare Workers + D1",
|
|
12
|
+
status: "active",
|
|
13
|
+
services: {
|
|
14
|
+
compute: "Cloudflare Workers via vinext",
|
|
15
|
+
relationalStorage: "D1 through the runtime SQL adapter",
|
|
16
|
+
objectStorage: "R2",
|
|
17
|
+
imageOptimization: "Cloudflare Images",
|
|
18
|
+
cache: "vinext CDN/data adapters and caches.default for media",
|
|
19
|
+
authStorage: "D1 users and signed cookies"
|
|
20
|
+
},
|
|
21
|
+
capabilities: [
|
|
22
|
+
"server-rendering",
|
|
23
|
+
"edge-cache",
|
|
24
|
+
"relational-storage",
|
|
25
|
+
"object-storage",
|
|
26
|
+
"image-optimization",
|
|
27
|
+
"secrets",
|
|
28
|
+
"observability"
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
var runtimeAdapters = [cloudflareWorkersAdapter];
|
|
32
|
+
function getRuntimeAdapter(id) {
|
|
33
|
+
return runtimeAdapters.find((adapter) => adapter.id === id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/platform/selection.ts
|
|
37
|
+
function currentRuntimeId() {
|
|
38
|
+
return "cloudflare-workers";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/doctor/doctor.ts
|
|
42
|
+
function envValue(env, name) {
|
|
43
|
+
const value = String(env[name] ?? "").trim();
|
|
44
|
+
return value || void 0;
|
|
45
|
+
}
|
|
46
|
+
function hasEnv(env, name) {
|
|
47
|
+
return Boolean(envValue(env, name));
|
|
48
|
+
}
|
|
49
|
+
function hasD1Binding(config, binding) {
|
|
50
|
+
return Boolean(config?.d1_databases?.some((item) => item.binding === binding));
|
|
51
|
+
}
|
|
52
|
+
function hasR2Binding(config, binding) {
|
|
53
|
+
return Boolean(config?.r2_buckets?.some((item) => item.binding === binding));
|
|
54
|
+
}
|
|
55
|
+
function hasImagesBinding(config, binding) {
|
|
56
|
+
const images = config?.images;
|
|
57
|
+
if (Array.isArray(images)) {
|
|
58
|
+
return images.some((item) => item.binding === binding);
|
|
59
|
+
}
|
|
60
|
+
return images?.binding === binding;
|
|
61
|
+
}
|
|
62
|
+
function statusSummary(status) {
|
|
63
|
+
if (status === "ok") return "ready";
|
|
64
|
+
if (status === "warn") return "usable with warnings";
|
|
65
|
+
return "missing required configuration";
|
|
66
|
+
}
|
|
67
|
+
function overallStatus(checks, models) {
|
|
68
|
+
if (checks.some((check) => check.status === "missing") || models.some((model) => model.dataSourceStatus === "missing")) {
|
|
69
|
+
return "missing";
|
|
70
|
+
}
|
|
71
|
+
if (checks.some((check) => check.status === "warn") || models.some((model) => model.dataSourceStatus === "warn")) {
|
|
72
|
+
return "warn";
|
|
73
|
+
}
|
|
74
|
+
return "ok";
|
|
75
|
+
}
|
|
76
|
+
function cloudflareChecks(config) {
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
id: "runtime.database",
|
|
80
|
+
label: "SQL database",
|
|
81
|
+
status: hasD1Binding(config, "DB") ? "ok" : "missing",
|
|
82
|
+
required: true,
|
|
83
|
+
detail: hasD1Binding(config, "DB") ? "wrangler D1 binding DB is declared" : "wrangler D1 binding DB is missing",
|
|
84
|
+
action: "Add a DB binding under d1_databases in wrangler.jsonc."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "runtime.objectStorage",
|
|
88
|
+
label: "Object storage",
|
|
89
|
+
status: hasR2Binding(config, "ASSETS_BUCKET") ? "ok" : "warn",
|
|
90
|
+
required: false,
|
|
91
|
+
detail: hasR2Binding(config, "ASSETS_BUCKET") ? "wrangler R2 binding ASSETS_BUCKET is declared" : "uploads and persistent media cache need ASSETS_BUCKET",
|
|
92
|
+
action: "Add an ASSETS_BUCKET R2 binding for uploads and media cache."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "runtime.imageTransformer",
|
|
96
|
+
label: "Image transformation",
|
|
97
|
+
status: hasImagesBinding(config, "IMAGES") ? "ok" : "warn",
|
|
98
|
+
required: false,
|
|
99
|
+
detail: hasImagesBinding(config, "IMAGES") ? "wrangler Images binding IMAGES is declared" : "Notion media optimization will fall back without IMAGES",
|
|
100
|
+
action: "Add a Cloudflare Images binding named IMAGES."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "runtime.publicCache",
|
|
104
|
+
label: "Public cache",
|
|
105
|
+
status: "ok",
|
|
106
|
+
required: true,
|
|
107
|
+
detail: "vinext CDN adapter handles page cache; caches.default remains for media"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "runtime.observability",
|
|
111
|
+
label: "Observability",
|
|
112
|
+
status: config?.observability?.enabled ? "ok" : "warn",
|
|
113
|
+
required: false,
|
|
114
|
+
detail: config?.observability?.enabled ? "wrangler observability is enabled" : "wrangler observability is not enabled",
|
|
115
|
+
action: "Enable observability in wrangler.jsonc for production debugging."
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
function notionChecks(env) {
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
id: "notion.token",
|
|
123
|
+
label: "Notion token",
|
|
124
|
+
status: hasEnv(env, "NOTION_TOKEN") ? "ok" : "missing",
|
|
125
|
+
required: true,
|
|
126
|
+
detail: hasEnv(env, "NOTION_TOKEN") ? "NOTION_TOKEN is configured" : "NOTION_TOKEN is missing",
|
|
127
|
+
action: "Set NOTION_TOKEN to an internal integration token."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "notion.webhook",
|
|
131
|
+
label: "Notion webhook verification",
|
|
132
|
+
status: hasEnv(env, "NOTION_WEBHOOK_VERIFICATION_TOKEN") ? "ok" : "warn",
|
|
133
|
+
required: false,
|
|
134
|
+
detail: hasEnv(env, "NOTION_WEBHOOK_VERIFICATION_TOKEN") ? "NOTION_WEBHOOK_VERIFICATION_TOKEN is configured" : "instant content invalidation needs NOTION_WEBHOOK_VERIFICATION_TOKEN",
|
|
135
|
+
action: "Set NOTION_WEBHOOK_VERIFICATION_TOKEN after creating the Notion webhook."
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
function modelDoctorStatus(env, model) {
|
|
140
|
+
const hasConfiguredEnv = hasEnv(env, model.source.dataSourceEnv);
|
|
141
|
+
const hasDefault = Boolean(model.source.defaultDataSourceId);
|
|
142
|
+
const dataSourceSource = hasConfiguredEnv ? "env" : hasDefault ? "default" : "missing";
|
|
143
|
+
const dataSourceStatus = dataSourceSource === "missing" ? "missing" : "ok";
|
|
144
|
+
return {
|
|
145
|
+
dataSourceStatus,
|
|
146
|
+
dataSourceSource
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function modelChecks(env, models) {
|
|
150
|
+
return models.map((model) => ({
|
|
151
|
+
id: model.id,
|
|
152
|
+
public: model.visibility.public,
|
|
153
|
+
admin: model.visibility.admin,
|
|
154
|
+
listPath: model.routes.listPath,
|
|
155
|
+
detailPath: model.routes.detailPath,
|
|
156
|
+
publicApiPath: model.routes.publicApiPath,
|
|
157
|
+
dataSourceEnv: model.source.dataSourceEnv,
|
|
158
|
+
...modelDoctorStatus(env, model)
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
function uniqueActions(checks) {
|
|
162
|
+
return Array.from(
|
|
163
|
+
new Set(
|
|
164
|
+
checks.filter((check) => check.status !== "ok" && check.action).map((check) => check.action)
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
function omitResolvedActions(check) {
|
|
169
|
+
if (check.status !== "ok") return check;
|
|
170
|
+
return {
|
|
171
|
+
...check,
|
|
172
|
+
action: void 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function buildNextionDoctorReport(options = {}) {
|
|
176
|
+
const env = options.env ?? process.env;
|
|
177
|
+
const runtimeId = options.runtimeId ?? currentRuntimeId();
|
|
178
|
+
const adapter = getRuntimeAdapter(runtimeId);
|
|
179
|
+
const checks = [
|
|
180
|
+
...cloudflareChecks(options.wranglerConfig),
|
|
181
|
+
...notionChecks(env)
|
|
182
|
+
].map(omitResolvedActions);
|
|
183
|
+
const models = modelChecks(env, options.models ?? []);
|
|
184
|
+
const status = overallStatus(checks, models);
|
|
185
|
+
const modelActions = models.filter((model) => model.dataSourceStatus === "missing").map(
|
|
186
|
+
(model) => `Set ${model.dataSourceEnv} for the ${model.id} content model or add a model defaultDataSourceId.`
|
|
187
|
+
);
|
|
188
|
+
return {
|
|
189
|
+
overall: {
|
|
190
|
+
status,
|
|
191
|
+
summary: statusSummary(status)
|
|
192
|
+
},
|
|
193
|
+
runtime: {
|
|
194
|
+
id: runtimeId,
|
|
195
|
+
label: adapter?.label ?? runtimeId,
|
|
196
|
+
adapterStatus: adapter?.status ?? "planned"
|
|
197
|
+
},
|
|
198
|
+
checks,
|
|
199
|
+
models,
|
|
200
|
+
nextSteps: [...uniqueActions(checks), ...modelActions]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function formatCheck(check) {
|
|
204
|
+
const required = check.required ? "required" : "optional";
|
|
205
|
+
return ` [${check.status}] ${check.label} (${required}) - ${check.detail}`;
|
|
206
|
+
}
|
|
207
|
+
function formatModel(model) {
|
|
208
|
+
const visibility = [
|
|
209
|
+
model.public ? "public" : "",
|
|
210
|
+
model.admin ? "admin" : ""
|
|
211
|
+
].filter(Boolean);
|
|
212
|
+
const source = model.dataSourceSource === "env" ? model.dataSourceEnv : model.dataSourceSource === "default" ? `${model.dataSourceEnv} or model default` : model.dataSourceEnv;
|
|
213
|
+
return [
|
|
214
|
+
` [${model.dataSourceStatus}] ${model.id} (${visibility.join(", ") || "private"})`,
|
|
215
|
+
` routes: ${model.listPath}, ${model.detailPath}${model.publicApiPath ? `, ${model.publicApiPath}` : ""}`,
|
|
216
|
+
` notion: ${source}`
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
219
|
+
function formatNextionDoctorReport(report) {
|
|
220
|
+
const lines = [
|
|
221
|
+
"vinext nextion doctor",
|
|
222
|
+
"",
|
|
223
|
+
`Overall: [${report.overall.status}] ${report.overall.summary}`,
|
|
224
|
+
`Runtime: ${report.runtime.label} (${report.runtime.id}, ${report.runtime.adapterStatus})`,
|
|
225
|
+
"",
|
|
226
|
+
"Checks:",
|
|
227
|
+
...report.checks.map(formatCheck),
|
|
228
|
+
"",
|
|
229
|
+
"Content models:",
|
|
230
|
+
...report.models.map(formatModel)
|
|
231
|
+
];
|
|
232
|
+
if (report.nextSteps.length > 0) {
|
|
233
|
+
lines.push("", "Next steps:");
|
|
234
|
+
for (const step of report.nextSteps) {
|
|
235
|
+
lines.push(` - ${step}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/doctor/cli.ts
|
|
242
|
+
var projectRoot = process.cwd();
|
|
243
|
+
function parseArgs(argv) {
|
|
244
|
+
const result = {
|
|
245
|
+
json: false
|
|
246
|
+
};
|
|
247
|
+
for (const arg of argv) {
|
|
248
|
+
if (arg === "--json") {
|
|
249
|
+
result.json = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
function stripJsonComments(source) {
|
|
257
|
+
let output = "";
|
|
258
|
+
let inString = false;
|
|
259
|
+
let escaped = false;
|
|
260
|
+
let lineComment = false;
|
|
261
|
+
let blockComment = false;
|
|
262
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
263
|
+
const char = source[index];
|
|
264
|
+
const next = source[index + 1];
|
|
265
|
+
if (lineComment) {
|
|
266
|
+
if (char === "\n") {
|
|
267
|
+
lineComment = false;
|
|
268
|
+
output += char;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (blockComment) {
|
|
273
|
+
if (char === "*" && next === "/") {
|
|
274
|
+
blockComment = false;
|
|
275
|
+
index += 1;
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (inString) {
|
|
280
|
+
output += char;
|
|
281
|
+
if (escaped) {
|
|
282
|
+
escaped = false;
|
|
283
|
+
} else if (char === "\\") {
|
|
284
|
+
escaped = true;
|
|
285
|
+
} else if (char === '"') {
|
|
286
|
+
inString = false;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (char === '"') {
|
|
291
|
+
inString = true;
|
|
292
|
+
output += char;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (char === "/" && next === "/") {
|
|
296
|
+
lineComment = true;
|
|
297
|
+
index += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (char === "/" && next === "*") {
|
|
301
|
+
blockComment = true;
|
|
302
|
+
index += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
output += char;
|
|
306
|
+
}
|
|
307
|
+
return output;
|
|
308
|
+
}
|
|
309
|
+
function readJsonc(filePath) {
|
|
310
|
+
if (!fs.existsSync(filePath)) return null;
|
|
311
|
+
return JSON.parse(stripJsonComments(fs.readFileSync(filePath, "utf8")));
|
|
312
|
+
}
|
|
313
|
+
function readDotEnvFile(filePath) {
|
|
314
|
+
if (!fs.existsSync(filePath)) return {};
|
|
315
|
+
const env = {};
|
|
316
|
+
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
317
|
+
const trimmed = line.trim();
|
|
318
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
319
|
+
const equalIndex = trimmed.indexOf("=");
|
|
320
|
+
if (equalIndex === -1) continue;
|
|
321
|
+
const name = trimmed.slice(0, equalIndex).trim();
|
|
322
|
+
let value = trimmed.slice(equalIndex + 1).trim();
|
|
323
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
324
|
+
value = value.slice(1, -1);
|
|
325
|
+
}
|
|
326
|
+
env[name] = value;
|
|
327
|
+
}
|
|
328
|
+
return env;
|
|
329
|
+
}
|
|
330
|
+
function mergedEnv(wranglerConfig) {
|
|
331
|
+
return {
|
|
332
|
+
...wranglerConfig?.vars,
|
|
333
|
+
...readDotEnvFile(path.join(projectRoot, ".env.local")),
|
|
334
|
+
...readDotEnvFile(path.join(projectRoot, ".dev.vars")),
|
|
335
|
+
...process.env
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function main() {
|
|
339
|
+
const args = parseArgs(process.argv.slice(2));
|
|
340
|
+
const wranglerConfig = readJsonc(path.join(projectRoot, "wrangler.jsonc"));
|
|
341
|
+
const report = buildNextionDoctorReport({
|
|
342
|
+
env: mergedEnv(wranglerConfig),
|
|
343
|
+
wranglerConfig
|
|
344
|
+
});
|
|
345
|
+
if (args.json) {
|
|
346
|
+
console.log(JSON.stringify(report, null, 2));
|
|
347
|
+
} else {
|
|
348
|
+
console.log(formatNextionDoctorReport(report));
|
|
349
|
+
}
|
|
350
|
+
if (report.overall.status === "missing") {
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? "")) {
|
|
355
|
+
main().catch((error) => {
|
|
356
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=cli.js.map
|