@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,67 @@
|
|
|
1
|
+
import { NextionMiddlewareOptions } from './middleware.js';
|
|
2
|
+
import { AdminNavItem, AuthConfig } from './types.js';
|
|
3
|
+
import { ContentSource } from './content/models.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Public surface for the bootstrap options. Mirrors the placeholder
|
|
7
|
+
* `WorkerOptions` type in `src/types.ts`; Phase 6 normalizes the
|
|
8
|
+
* shape once the content abstraction lands.
|
|
9
|
+
*/
|
|
10
|
+
interface FoundationWorkerOptions {
|
|
11
|
+
sources: ContentSource[];
|
|
12
|
+
adminNav: AdminNavItem[];
|
|
13
|
+
authConfig: AuthConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Placeholder for the future site config. Kept on the options
|
|
16
|
+
* surface so consumers can wire it today; the bootstrap itself
|
|
17
|
+
* does not yet consume it.
|
|
18
|
+
*/
|
|
19
|
+
siteConfig: {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
defaultLocale: string;
|
|
23
|
+
locales: string[];
|
|
24
|
+
navigation: unknown[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Optional session resolver passed straight through to
|
|
28
|
+
* `nextionMiddleware`. When omitted the middleware still runs
|
|
29
|
+
* the admin gate but always reports no viewer.
|
|
30
|
+
*/
|
|
31
|
+
sessionLookup?: NextionMiddlewareOptions["sessionLookup"];
|
|
32
|
+
/**
|
|
33
|
+
* Project-injected routes. Each entry maps a pathname to a
|
|
34
|
+
* dynamic loader. The loader returns a module whose `default`
|
|
35
|
+
* export is a `(request, options) => Promise<Response>` handler.
|
|
36
|
+
*
|
|
37
|
+
* Loaders are invoked lazily on the first matching request so
|
|
38
|
+
* the route module can pull in heavy dependencies (e.g. the
|
|
39
|
+
* starter's content models) only when needed.
|
|
40
|
+
*/
|
|
41
|
+
extraRoutes?: Record<string, () => Promise<{
|
|
42
|
+
default: FoundationExtraRouteHandler;
|
|
43
|
+
}>>;
|
|
44
|
+
}
|
|
45
|
+
type FoundationExtraRouteHandler = (request: Request, options: FoundationWorkerOptions, sources: ContentSource[], auth: {
|
|
46
|
+
databaseBinding: string;
|
|
47
|
+
}) => Promise<Response>;
|
|
48
|
+
interface FoundationWorker {
|
|
49
|
+
/**
|
|
50
|
+
* Worker-style fetch. Returns `Response` when foundation handled
|
|
51
|
+
* the request, `null` to let the caller fall through to vinext.
|
|
52
|
+
*/
|
|
53
|
+
fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response | null>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a foundation Cloudflare Worker.
|
|
57
|
+
*
|
|
58
|
+
* The returned object exposes a `fetch` method that:
|
|
59
|
+
* 1. Runs `nextionMiddleware` (admin gate + viewer attachment).
|
|
60
|
+
* 2. Tries the first-party route table.
|
|
61
|
+
* 3. Tries any project-injected `extraRoutes`.
|
|
62
|
+
* 4. Returns `null` if nothing matched, so the starter wrapper can
|
|
63
|
+
* delegate to vinext.
|
|
64
|
+
*/
|
|
65
|
+
declare function createNextionWorker(options: FoundationWorkerOptions): FoundationWorker;
|
|
66
|
+
|
|
67
|
+
export { type FoundationExtraRouteHandler as F, type FoundationWorker as a, type FoundationWorkerOptions as b, createNextionWorker as c };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type PublicMediaVariant = "avif" | "webp" | "source";
|
|
2
|
+
declare function publicMediaVariantForAccept(accept: string): PublicMediaVariant;
|
|
3
|
+
declare function publicMediaCacheKeyForUrl(input: URL, variant: PublicMediaVariant): string;
|
|
4
|
+
declare function notionMediaR2KeyForUrl(input: URL, variant: PublicMediaVariant): string | null;
|
|
5
|
+
|
|
6
|
+
export { type PublicMediaVariant, notionMediaR2KeyForUrl, publicMediaCacheKeyForUrl, publicMediaVariantForAccept };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/cache/cache-keys.ts
|
|
2
|
+
var CACHE_ORIGIN = "https://cache.local";
|
|
3
|
+
var CACHE_NAMESPACE = "/__public-cache/v20260609a";
|
|
4
|
+
var NOTION_MEDIA_R2_PREFIX = "notion-media/v1";
|
|
5
|
+
function normalizePath(pathname) {
|
|
6
|
+
if (pathname === "/") return "/";
|
|
7
|
+
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
8
|
+
}
|
|
9
|
+
function publicMediaVariantForAccept(accept) {
|
|
10
|
+
if (accept.includes("image/avif")) return "avif";
|
|
11
|
+
if (accept.includes("image/webp")) return "webp";
|
|
12
|
+
return "source";
|
|
13
|
+
}
|
|
14
|
+
function publicMediaCacheKeyForUrl(input, variant) {
|
|
15
|
+
const url = new URL(
|
|
16
|
+
`${CACHE_NAMESPACE}${normalizePath(input.pathname)}${input.search}`,
|
|
17
|
+
CACHE_ORIGIN
|
|
18
|
+
);
|
|
19
|
+
url.searchParams.set("__variant", variant);
|
|
20
|
+
url.searchParams.sort();
|
|
21
|
+
return url.toString();
|
|
22
|
+
}
|
|
23
|
+
function keySegment(value) {
|
|
24
|
+
return encodeURIComponent(value || "none");
|
|
25
|
+
}
|
|
26
|
+
function notionMediaR2KeyForUrl(input, variant) {
|
|
27
|
+
if (variant === "source") return null;
|
|
28
|
+
const version = input.searchParams.get("v");
|
|
29
|
+
if (!version) return null;
|
|
30
|
+
const path = normalizePath(input.pathname).split("/").filter(Boolean).map(keySegment).join("/");
|
|
31
|
+
const width = input.searchParams.get("w") ?? "source";
|
|
32
|
+
const quality = input.searchParams.get("q") ?? "source";
|
|
33
|
+
return [
|
|
34
|
+
NOTION_MEDIA_R2_PREFIX,
|
|
35
|
+
variant,
|
|
36
|
+
path,
|
|
37
|
+
`v-${keySegment(version)}`,
|
|
38
|
+
`w-${keySegment(width)}`,
|
|
39
|
+
`q-${keySegment(quality)}.${variant}`
|
|
40
|
+
].join("/");
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
notionMediaR2KeyForUrl,
|
|
44
|
+
publicMediaCacheKeyForUrl,
|
|
45
|
+
publicMediaVariantForAccept
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cache/cache-keys.ts"],"sourcesContent":["// Edge cache keys for transformed Notion media responses.\n//\n// Page and API route caching is handled by vinext's CDN cache adapter\n// (vite.config.ts). These helpers remain for the media proxy route.\n\nconst CACHE_ORIGIN = \"https://cache.local\";\nconst CACHE_NAMESPACE = \"/__public-cache/v20260609a\";\nconst NOTION_MEDIA_R2_PREFIX = \"notion-media/v1\";\n\nexport type PublicMediaVariant = \"avif\" | \"webp\" | \"source\";\n\nfunction normalizePath(pathname: string) {\n if (pathname === \"/\") return \"/\";\n return pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n}\n\nexport function publicMediaVariantForAccept(accept: string): PublicMediaVariant {\n if (accept.includes(\"image/avif\")) return \"avif\";\n if (accept.includes(\"image/webp\")) return \"webp\";\n return \"source\";\n}\n\nexport function publicMediaCacheKeyForUrl(\n input: URL,\n variant: PublicMediaVariant\n) {\n const url = new URL(\n `${CACHE_NAMESPACE}${normalizePath(input.pathname)}${input.search}`,\n CACHE_ORIGIN\n );\n url.searchParams.set(\"__variant\", variant);\n url.searchParams.sort();\n return url.toString();\n}\n\nfunction keySegment(value: string) {\n return encodeURIComponent(value || \"none\");\n}\n\nexport function notionMediaR2KeyForUrl(\n input: URL,\n variant: PublicMediaVariant\n) {\n if (variant === \"source\") return null;\n\n const version = input.searchParams.get(\"v\");\n if (!version) return null;\n\n const path = normalizePath(input.pathname)\n .split(\"/\")\n .filter(Boolean)\n .map(keySegment)\n .join(\"/\");\n const width = input.searchParams.get(\"w\") ?? \"source\";\n const quality = input.searchParams.get(\"q\") ?? \"source\";\n\n return [\n NOTION_MEDIA_R2_PREFIX,\n variant,\n path,\n `v-${keySegment(version)}`,\n `w-${keySegment(width)}`,\n `q-${keySegment(quality)}.${variant}`,\n ].join(\"/\");\n}\n"],"mappings":";AAKA,IAAM,eAAe;AACrB,IAAM,kBAAkB;AACxB,IAAM,yBAAyB;AAI/B,SAAS,cAAc,UAAkB;AACvC,MAAI,aAAa,IAAK,QAAO;AAC7B,SAAO,SAAS,SAAS,GAAG,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;AAC1D;AAEO,SAAS,4BAA4B,QAAoC;AAC9E,MAAI,OAAO,SAAS,YAAY,EAAG,QAAO;AAC1C,MAAI,OAAO,SAAS,YAAY,EAAG,QAAO;AAC1C,SAAO;AACT;AAEO,SAAS,0BACd,OACA,SACA;AACA,QAAM,MAAM,IAAI;AAAA,IACd,GAAG,eAAe,GAAG,cAAc,MAAM,QAAQ,CAAC,GAAG,MAAM,MAAM;AAAA,IACjE;AAAA,EACF;AACA,MAAI,aAAa,IAAI,aAAa,OAAO;AACzC,MAAI,aAAa,KAAK;AACtB,SAAO,IAAI,SAAS;AACtB;AAEA,SAAS,WAAW,OAAe;AACjC,SAAO,mBAAmB,SAAS,MAAM;AAC3C;AAEO,SAAS,uBACd,OACA,SACA;AACA,MAAI,YAAY,SAAU,QAAO;AAEjC,QAAM,UAAU,MAAM,aAAa,IAAI,GAAG;AAC1C,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,OAAO,cAAc,MAAM,QAAQ,EACtC,MAAM,GAAG,EACT,OAAO,OAAO,EACd,IAAI,UAAU,EACd,KAAK,GAAG;AACX,QAAM,QAAQ,MAAM,aAAa,IAAI,GAAG,KAAK;AAC7C,QAAM,UAAU,MAAM,aAAa,IAAI,GAAG,KAAK;AAE/C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,WAAW,OAAO,CAAC;AAAA,IACxB,KAAK,WAAW,KAAK,CAAC;AAAA,IACtB,KAAK,WAAW,OAAO,CAAC,IAAI,OAAO;AAAA,EACrC,EAAE,KAAK,GAAG;AACZ;","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ContentModelDefinition } from './models.js';
|
|
2
|
+
import { NotionFieldMap } from '../notion/types.js';
|
|
3
|
+
|
|
4
|
+
type ContentModelAdminSummary = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
kind: ContentModelDefinition["kind"];
|
|
8
|
+
visibility: "public" | "admin" | "public+admin" | "private";
|
|
9
|
+
listPath: string;
|
|
10
|
+
detailPath: string;
|
|
11
|
+
publicApiPath?: string;
|
|
12
|
+
dataSourceEnv: string;
|
|
13
|
+
hasDefaultDataSource: boolean;
|
|
14
|
+
fieldCount: number;
|
|
15
|
+
capabilities: {
|
|
16
|
+
richBlocks: boolean;
|
|
17
|
+
coverImages: boolean;
|
|
18
|
+
gatedAssets: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
declare function summarizeContentModelForAdmin(model: ContentModelDefinition<NotionFieldMap>): ContentModelAdminSummary;
|
|
22
|
+
declare function getContentModelAdminSummaries(models?: readonly ContentModelDefinition<NotionFieldMap>[]): ContentModelAdminSummary[];
|
|
23
|
+
|
|
24
|
+
export { type ContentModelAdminSummary, getContentModelAdminSummaries, summarizeContentModelForAdmin };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/content/models.ts
|
|
2
|
+
var registry = [];
|
|
3
|
+
function getRegisteredSources() {
|
|
4
|
+
return registry;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/content/admin-summary.ts
|
|
8
|
+
function visibilityFor(model) {
|
|
9
|
+
if (model.visibility.public && model.visibility.admin) return "public+admin";
|
|
10
|
+
if (model.visibility.public) return "public";
|
|
11
|
+
if (model.visibility.admin) return "admin";
|
|
12
|
+
return "private";
|
|
13
|
+
}
|
|
14
|
+
function summarizeContentModelForAdmin(model) {
|
|
15
|
+
return {
|
|
16
|
+
id: model.id,
|
|
17
|
+
name: model.ui.name,
|
|
18
|
+
kind: model.kind,
|
|
19
|
+
visibility: visibilityFor(model),
|
|
20
|
+
listPath: model.routes.listPath,
|
|
21
|
+
detailPath: model.routes.detailPath,
|
|
22
|
+
publicApiPath: model.routes.publicApiPath,
|
|
23
|
+
dataSourceEnv: model.source.dataSourceEnv,
|
|
24
|
+
hasDefaultDataSource: Boolean(model.source.defaultDataSourceId),
|
|
25
|
+
fieldCount: Object.keys(model.source.fields).length,
|
|
26
|
+
capabilities: model.capabilities
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function getContentModelAdminSummaries(models = getRegisteredSources()) {
|
|
30
|
+
return models.map(summarizeContentModelForAdmin);
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
getContentModelAdminSummaries,
|
|
34
|
+
summarizeContentModelForAdmin
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=admin-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/content/models.ts","../../src/content/admin-summary.ts"],"sourcesContent":["// packages/nextion/src/content/models.ts\n//\n// Canonical content-source shape and a module-level registry.\n//\n// The starter's `defineContentModel` returns the same value passed in.\n// The foundation's `defineContentSource` adds a side effect: it stores\n// the value in a process-wide registry so other packages (admin pages,\n// search index, revalidation) can discover content sources without\n// reaching back into the starter.\n//\n// Existing values from the starter pass through unchanged: `ContentSource`\n// is a structural alias of the prior `ContentModelDefinition<TFields>` so\n// source field-maps stay narrowly typed through the boundary.\n\nimport type {\n NotionFieldMap,\n NotionSort,\n NotionSortDirection,\n} from \"../notion/types\";\n\nexport type { NotionFieldMap, NotionSort, NotionSortDirection };\n\n/**\n * Canonical content-source shape. The shape mirrors the starter's\n * prior `ContentModelDefinition` exactly so that registered sources\n * remain type-compatible across the package boundary.\n */\nexport type ContentModelDefinition<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = {\n id: string;\n kind: \"article\" | \"catalog\" | \"directory\";\n visibility: {\n public: boolean;\n admin: boolean;\n };\n source: {\n type: \"notion\";\n tokenEnv: \"NOTION_TOKEN\";\n dataSourceEnv: string;\n defaultDataSourceId?: string;\n fields: TFields;\n query: {\n pageSize: number;\n sorts?: readonly NotionSort[];\n filterProperties?: readonly string[];\n };\n };\n routes: {\n listPath: string;\n detailPath: string;\n detailParam: string;\n publicApiPath?: string;\n };\n ui: {\n name: string;\n pluralName: string;\n navLabel: string;\n listTitle: string;\n listDescription: string;\n emptyState: string;\n };\n capabilities: {\n richBlocks: boolean;\n coverImages: boolean;\n gatedAssets: boolean;\n };\n};\n\n/**\n * Public alias for `ContentModelDefinition`. External consumers import\n * this name from `@notionx/core/content`; the internal\n * `ContentModelDefinition` name remains available for the starter's\n * `model.ts`.\n */\nexport type ContentSource<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = ContentModelDefinition<TFields>;\n\nconst registry: ContentSource[] = [];\n\n/**\n * Register a content source. Returns the value unchanged. Re-registering\n * the same `id` replaces the prior value (idempotent on the id, useful\n * for HMR + tests).\n */\nexport function defineContentSource<const TFields extends NotionFieldMap>(\n model: ContentModelDefinition<TFields>\n): ContentModelDefinition<TFields> {\n const existing = registry.findIndex((s) => s.id === model.id);\n if (existing >= 0) registry[existing] = model;\n else registry.push(model);\n return model;\n}\n\nexport function getRegisteredSources(): readonly ContentSource[] {\n return registry;\n}\n\nexport function getRegisteredSource(id: string): ContentSource | undefined {\n return registry.find((s) => s.id === id);\n}\n\n/**\n * Test-only escape hatch: empties the registry so vitest cases do not\n * leak state between files. Not for production use.\n */\nexport function clearRegistryForTests(): void {\n registry.length = 0;\n}\n","// packages/nextion/src/content/admin-summary.ts\n//\n// Generic admin summary helpers for content sources.\n\nimport type {\n ContentModelDefinition,\n NotionFieldMap,\n} from \"./models\";\nimport { getRegisteredSources } from \"./models\";\n\nexport type ContentModelAdminSummary = {\n id: string;\n name: string;\n kind: ContentModelDefinition[\"kind\"];\n visibility: \"public\" | \"admin\" | \"public+admin\" | \"private\";\n listPath: string;\n detailPath: string;\n publicApiPath?: string;\n dataSourceEnv: string;\n hasDefaultDataSource: boolean;\n fieldCount: number;\n capabilities: {\n richBlocks: boolean;\n coverImages: boolean;\n gatedAssets: boolean;\n };\n};\n\nfunction visibilityFor(model: ContentModelDefinition<NotionFieldMap>) {\n if (model.visibility.public && model.visibility.admin) return \"public+admin\";\n if (model.visibility.public) return \"public\";\n if (model.visibility.admin) return \"admin\";\n return \"private\";\n}\n\nexport function summarizeContentModelForAdmin(\n model: ContentModelDefinition<NotionFieldMap>\n): ContentModelAdminSummary {\n return {\n id: model.id,\n name: model.ui.name,\n kind: model.kind,\n visibility: visibilityFor(model),\n listPath: model.routes.listPath,\n detailPath: model.routes.detailPath,\n publicApiPath: model.routes.publicApiPath,\n dataSourceEnv: model.source.dataSourceEnv,\n hasDefaultDataSource: Boolean(model.source.defaultDataSourceId),\n fieldCount: Object.keys(model.source.fields).length,\n capabilities: model.capabilities,\n };\n}\n\nexport function getContentModelAdminSummaries(\n models: readonly ContentModelDefinition<NotionFieldMap>[] = getRegisteredSources()\n) {\n return models.map(summarizeContentModelForAdmin);\n}\n"],"mappings":";AA+EA,IAAM,WAA4B,CAAC;AAgB5B,SAAS,uBAAiD;AAC/D,SAAO;AACT;;;ACrEA,SAAS,cAAc,OAA+C;AACpE,MAAI,MAAM,WAAW,UAAU,MAAM,WAAW,MAAO,QAAO;AAC9D,MAAI,MAAM,WAAW,OAAQ,QAAO;AACpC,MAAI,MAAM,WAAW,MAAO,QAAO;AACnC,SAAO;AACT;AAEO,SAAS,8BACd,OAC0B;AAC1B,SAAO;AAAA,IACL,IAAI,MAAM;AAAA,IACV,MAAM,MAAM,GAAG;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,YAAY,cAAc,KAAK;AAAA,IAC/B,UAAU,MAAM,OAAO;AAAA,IACvB,YAAY,MAAM,OAAO;AAAA,IACzB,eAAe,MAAM,OAAO;AAAA,IAC5B,eAAe,MAAM,OAAO;AAAA,IAC5B,sBAAsB,QAAQ,MAAM,OAAO,mBAAmB;AAAA,IAC9D,YAAY,OAAO,KAAK,MAAM,OAAO,MAAM,EAAE;AAAA,IAC7C,cAAc,MAAM;AAAA,EACtB;AACF;AAEO,SAAS,8BACd,SAA4D,qBAAqB,GACjF;AACA,SAAO,OAAO,IAAI,6BAA6B;AACjD;","names":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { ContentModelDefinition, ContentSource, clearRegistryForTests, defineContentSource, getRegisteredSource, getRegisteredSources } from './models.js';
|
|
2
|
+
export { ContentRevalidateRequest, InvalidationKind, RevalidatePathFn, authorizeContentRevalidate, buildContentRevalidationPaths, previewContentModelInvalidation, readContentRevalidateRequest, readContentRevalidateRequestFromUrl } from './revalidate.js';
|
|
3
|
+
export { SearchIndexDocument, SearchIndexedItem, deleteSearchIndexDocument, deleteSearchIndexForModel, filterItemsBySearchIndex, getMissingSearchIndexRouteIds, matchesIndexedItem, querySearchIndexRouteIds, upsertSearchIndexDocument } from './search-index.js';
|
|
4
|
+
export { filterMoviesBySearch, filterPostsBySearch, matchesSearchQuery, normalizeSearchQuery } from './search.js';
|
|
5
|
+
export { ContentPrewarmModelResult, ContentPrewarmResult, PrewarmTarget, prewarmPublicContentSearchIndex } from './prewarm.js';
|
|
6
|
+
export { ContentModelAdminSummary, getContentModelAdminSummaries, summarizeContentModelForAdmin } from './admin-summary.js';
|
|
7
|
+
export { NotionFieldMap, NotionSort, NotionSortDirection } from '../notion/types.js';
|
|
8
|
+
import '../platform/runtime.js';
|
|
9
|
+
import '../env-C5qu-0R-.js';
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// src/content/models.ts
|
|
2
|
+
var registry = [];
|
|
3
|
+
function defineContentSource(model) {
|
|
4
|
+
const existing = registry.findIndex((s) => s.id === model.id);
|
|
5
|
+
if (existing >= 0) registry[existing] = model;
|
|
6
|
+
else registry.push(model);
|
|
7
|
+
return model;
|
|
8
|
+
}
|
|
9
|
+
function getRegisteredSources() {
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
12
|
+
function getRegisteredSource(id) {
|
|
13
|
+
return registry.find((s) => s.id === id);
|
|
14
|
+
}
|
|
15
|
+
function clearRegistryForTests() {
|
|
16
|
+
registry.length = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/i18n/config.ts
|
|
20
|
+
var supportedLocales = ["zh-CN", "en-US"];
|
|
21
|
+
function isAppLocale(value) {
|
|
22
|
+
return supportedLocales.includes(value);
|
|
23
|
+
}
|
|
24
|
+
function expandLocalizedMoviePaths(paths, locale) {
|
|
25
|
+
const locales = locale && isAppLocale(locale) ? [locale] : [...supportedLocales];
|
|
26
|
+
const expanded = [];
|
|
27
|
+
for (const path of paths) {
|
|
28
|
+
if (path === "/movies" || path.startsWith("/movies/")) {
|
|
29
|
+
for (const currentLocale of locales) {
|
|
30
|
+
expanded.push(`/${currentLocale}${path}`);
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
expanded.push(path);
|
|
35
|
+
}
|
|
36
|
+
return Array.from(new Set(expanded));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/content/revalidate.ts
|
|
40
|
+
function asObject(input) {
|
|
41
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : null;
|
|
42
|
+
}
|
|
43
|
+
function readString(input, key) {
|
|
44
|
+
const value = input[key];
|
|
45
|
+
return typeof value === "string" ? value.trim() : "";
|
|
46
|
+
}
|
|
47
|
+
function readKind(input) {
|
|
48
|
+
const value = readString(input, "kind");
|
|
49
|
+
if (value === "publish" || value === "delete" || value === "update") {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
return "update";
|
|
53
|
+
}
|
|
54
|
+
function detailPathForRouteId(detailPath, routeId) {
|
|
55
|
+
return detailPath.replace(/\[[^\]]+\]/g, routeId);
|
|
56
|
+
}
|
|
57
|
+
function publicApiDetailPathForRouteId(publicApiPath, routeId) {
|
|
58
|
+
return `${publicApiPath.replace(/\/+$/, "")}/${routeId.replace(/^\/+/, "")}`;
|
|
59
|
+
}
|
|
60
|
+
function bearerToken(request) {
|
|
61
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
62
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
63
|
+
return match?.[1]?.trim() ?? "";
|
|
64
|
+
}
|
|
65
|
+
function timingSafeEqualString(a, b) {
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
const left = encoder.encode(a);
|
|
68
|
+
const right = encoder.encode(b);
|
|
69
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
70
|
+
let diff = 0;
|
|
71
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
72
|
+
diff |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
73
|
+
}
|
|
74
|
+
return diff === 0;
|
|
75
|
+
}
|
|
76
|
+
function shouldLocalizeMoviePaths(modelId) {
|
|
77
|
+
return modelId === "movies" || modelId === "movie-translations";
|
|
78
|
+
}
|
|
79
|
+
function buildContentRevalidationPaths(input) {
|
|
80
|
+
const pagePaths = [input.model.routes.listPath];
|
|
81
|
+
const routePaths = [];
|
|
82
|
+
if (input.routeId) {
|
|
83
|
+
pagePaths.push(
|
|
84
|
+
detailPathForRouteId(input.model.routes.detailPath, input.routeId)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (input.previousRouteId) {
|
|
88
|
+
pagePaths.push(
|
|
89
|
+
detailPathForRouteId(
|
|
90
|
+
input.model.routes.detailPath,
|
|
91
|
+
input.previousRouteId
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (input.localizedMovieDetailPaths?.length) {
|
|
96
|
+
pagePaths.push(...input.localizedMovieDetailPaths);
|
|
97
|
+
}
|
|
98
|
+
if (input.includeApi !== false && input.model.routes.publicApiPath) {
|
|
99
|
+
routePaths.push(input.model.routes.publicApiPath);
|
|
100
|
+
if (input.routeId) {
|
|
101
|
+
routePaths.push(
|
|
102
|
+
publicApiDetailPathForRouteId(
|
|
103
|
+
input.model.routes.publicApiPath,
|
|
104
|
+
input.routeId
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (input.previousRouteId) {
|
|
109
|
+
routePaths.push(
|
|
110
|
+
publicApiDetailPathForRouteId(
|
|
111
|
+
input.model.routes.publicApiPath,
|
|
112
|
+
input.previousRouteId
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const localizedPagePaths = shouldLocalizeMoviePaths(input.model.id) ? expandLocalizedMoviePaths(pagePaths, input.locale) : pagePaths;
|
|
118
|
+
return {
|
|
119
|
+
pagePaths: Array.from(new Set(localizedPagePaths)),
|
|
120
|
+
routePaths: Array.from(new Set(routePaths)),
|
|
121
|
+
all: Array.from(/* @__PURE__ */ new Set([...localizedPagePaths, ...routePaths]))
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function authorizeContentRevalidate(request, token) {
|
|
125
|
+
const expected = String(token ?? "").trim();
|
|
126
|
+
if (!expected) return false;
|
|
127
|
+
const actual = bearerToken(request);
|
|
128
|
+
return Boolean(actual && timingSafeEqualString(actual, expected));
|
|
129
|
+
}
|
|
130
|
+
async function readContentRevalidateRequest(request) {
|
|
131
|
+
const body = asObject(await request.json().catch(() => null));
|
|
132
|
+
if (!body) return null;
|
|
133
|
+
const modelId = readString(body, "modelId");
|
|
134
|
+
const pageId = readString(body, "pageId");
|
|
135
|
+
const routeId = readString(body, "routeId");
|
|
136
|
+
const previousRouteId = readString(body, "previousRouteId");
|
|
137
|
+
const locale = readString(body, "locale");
|
|
138
|
+
return {
|
|
139
|
+
modelId,
|
|
140
|
+
pageId: pageId || void 0,
|
|
141
|
+
routeId: routeId || void 0,
|
|
142
|
+
previousRouteId: previousRouteId || void 0,
|
|
143
|
+
locale: locale || void 0,
|
|
144
|
+
kind: readKind(body),
|
|
145
|
+
includeApi: body.includeApi !== false
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function readContentRevalidateRequestFromUrl(url) {
|
|
149
|
+
const modelId = url.searchParams.get("modelId")?.trim() ?? "";
|
|
150
|
+
if (!modelId) return null;
|
|
151
|
+
const kind = url.searchParams.get("kind")?.trim() ?? "";
|
|
152
|
+
const includeApi = url.searchParams.get("includeApi");
|
|
153
|
+
return {
|
|
154
|
+
modelId,
|
|
155
|
+
pageId: url.searchParams.get("pageId")?.trim() || void 0,
|
|
156
|
+
routeId: url.searchParams.get("routeId")?.trim() || void 0,
|
|
157
|
+
previousRouteId: url.searchParams.get("previousRouteId")?.trim() || void 0,
|
|
158
|
+
locale: url.searchParams.get("locale")?.trim() || void 0,
|
|
159
|
+
kind: kind === "publish" || kind === "delete" || kind === "update" ? kind : "update",
|
|
160
|
+
includeApi: includeApi !== "false"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function previewContentModelInvalidation(input) {
|
|
164
|
+
const model = getRegisteredSource(input.modelId);
|
|
165
|
+
if (!model) {
|
|
166
|
+
throw new Error(`Unknown content model: ${input.modelId}`);
|
|
167
|
+
}
|
|
168
|
+
return buildContentRevalidationPaths({
|
|
169
|
+
model,
|
|
170
|
+
routeId: input.routeId,
|
|
171
|
+
previousRouteId: input.previousRouteId,
|
|
172
|
+
includeApi: input.includeApi
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/content/search.ts
|
|
177
|
+
function normalizeSearchQuery(query) {
|
|
178
|
+
return String(query ?? "").normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
function searchTerms(query) {
|
|
181
|
+
const normalized = normalizeSearchQuery(query);
|
|
182
|
+
return normalized ? normalized.split(" ") : [];
|
|
183
|
+
}
|
|
184
|
+
function searchableText(values) {
|
|
185
|
+
return values.flatMap((value) => Array.isArray(value) ? value : [value]).filter(
|
|
186
|
+
(value) => ["string", "number", "boolean"].includes(typeof value)
|
|
187
|
+
).map((value) => String(value)).join(" ").normalize("NFKC").toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
function matchesSearchQuery(values, query) {
|
|
190
|
+
const terms = searchTerms(query);
|
|
191
|
+
if (terms.length === 0) return true;
|
|
192
|
+
const haystack = searchableText(values);
|
|
193
|
+
return terms.every((term) => haystack.includes(term));
|
|
194
|
+
}
|
|
195
|
+
function filterPostsBySearch(posts, query) {
|
|
196
|
+
return posts.filter(
|
|
197
|
+
(post) => matchesSearchQuery(
|
|
198
|
+
[
|
|
199
|
+
post.title,
|
|
200
|
+
post.description,
|
|
201
|
+
post.author,
|
|
202
|
+
post.tags,
|
|
203
|
+
post.slug,
|
|
204
|
+
post.date
|
|
205
|
+
],
|
|
206
|
+
query
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function filterMoviesBySearch(movies, query) {
|
|
211
|
+
return movies.filter(
|
|
212
|
+
(movie) => matchesSearchQuery(
|
|
213
|
+
[
|
|
214
|
+
movie.title,
|
|
215
|
+
movie.summary,
|
|
216
|
+
movie.director,
|
|
217
|
+
movie.actors,
|
|
218
|
+
movie.genres,
|
|
219
|
+
movie.releaseDate,
|
|
220
|
+
movie.routeId
|
|
221
|
+
],
|
|
222
|
+
query
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/content/search-index.ts
|
|
228
|
+
function indexValuesForItem(item) {
|
|
229
|
+
return [
|
|
230
|
+
item.title,
|
|
231
|
+
item.description,
|
|
232
|
+
item.author,
|
|
233
|
+
item.tags,
|
|
234
|
+
item.slug,
|
|
235
|
+
item.date,
|
|
236
|
+
item.summary,
|
|
237
|
+
item.director,
|
|
238
|
+
item.actors,
|
|
239
|
+
item.genres,
|
|
240
|
+
item.routeId,
|
|
241
|
+
item.releaseDate
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
function routeIdForItem(item) {
|
|
245
|
+
return item.routeId ?? item.slug ?? "";
|
|
246
|
+
}
|
|
247
|
+
function routeOrder(items) {
|
|
248
|
+
const order = /* @__PURE__ */ new Map();
|
|
249
|
+
items.forEach((item, index) => {
|
|
250
|
+
const routeId = routeIdForItem(item);
|
|
251
|
+
if (routeId) order.set(routeId, index);
|
|
252
|
+
});
|
|
253
|
+
return order;
|
|
254
|
+
}
|
|
255
|
+
function uniqueValues(values) {
|
|
256
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
257
|
+
}
|
|
258
|
+
async function upsertSearchIndexDocument(db, document) {
|
|
259
|
+
const normalizedText = [
|
|
260
|
+
document.title,
|
|
261
|
+
document.summary,
|
|
262
|
+
document.bodyText,
|
|
263
|
+
...document.facets
|
|
264
|
+
].join(" ").normalize("NFKC").replace(/\s+/g, " ").toLowerCase();
|
|
265
|
+
await db.prepare(
|
|
266
|
+
`INSERT INTO content_search_index (
|
|
267
|
+
model_id,
|
|
268
|
+
page_id,
|
|
269
|
+
route_id,
|
|
270
|
+
title,
|
|
271
|
+
summary,
|
|
272
|
+
body_text,
|
|
273
|
+
facets,
|
|
274
|
+
normalized_text,
|
|
275
|
+
source_updated_at,
|
|
276
|
+
indexed_at
|
|
277
|
+
)
|
|
278
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
279
|
+
ON CONFLICT(model_id, route_id) DO UPDATE SET
|
|
280
|
+
page_id = excluded.page_id,
|
|
281
|
+
title = excluded.title,
|
|
282
|
+
summary = excluded.summary,
|
|
283
|
+
body_text = excluded.body_text,
|
|
284
|
+
facets = excluded.facets,
|
|
285
|
+
normalized_text = excluded.normalized_text,
|
|
286
|
+
source_updated_at = excluded.source_updated_at,
|
|
287
|
+
indexed_at = excluded.indexed_at`
|
|
288
|
+
).bind(
|
|
289
|
+
document.modelId,
|
|
290
|
+
document.pageId,
|
|
291
|
+
document.routeId,
|
|
292
|
+
document.title,
|
|
293
|
+
document.summary,
|
|
294
|
+
document.bodyText,
|
|
295
|
+
JSON.stringify(uniqueValues([...document.facets])),
|
|
296
|
+
normalizedText,
|
|
297
|
+
document.sourceUpdatedAt ?? null
|
|
298
|
+
).run();
|
|
299
|
+
}
|
|
300
|
+
async function deleteSearchIndexDocument(db, input) {
|
|
301
|
+
await db.prepare(
|
|
302
|
+
"DELETE FROM content_search_index WHERE model_id = ? AND route_id = ?"
|
|
303
|
+
).bind(input.modelId, input.routeId).run();
|
|
304
|
+
}
|
|
305
|
+
async function deleteSearchIndexForModel(db, input) {
|
|
306
|
+
await db.prepare("DELETE FROM content_search_index WHERE model_id = ?").bind(input.modelId).run();
|
|
307
|
+
}
|
|
308
|
+
async function getMissingSearchIndexRouteIds(db, input) {
|
|
309
|
+
const routeIds = uniqueValues([...input.routeIds]);
|
|
310
|
+
if (routeIds.length === 0) return [];
|
|
311
|
+
const placeholders = routeIds.map(() => "?").join(", ");
|
|
312
|
+
const result = await db.prepare(
|
|
313
|
+
`SELECT route_id
|
|
314
|
+
FROM content_search_index
|
|
315
|
+
WHERE model_id = ? AND route_id IN (${placeholders})`
|
|
316
|
+
).bind(input.modelId, ...routeIds).all();
|
|
317
|
+
const present = new Set((result.results ?? []).map((row) => row.route_id));
|
|
318
|
+
return routeIds.filter((routeId) => !present.has(routeId));
|
|
319
|
+
}
|
|
320
|
+
async function querySearchIndexRouteIds(db, input) {
|
|
321
|
+
const query = normalizeSearchQuery(input.query);
|
|
322
|
+
if (!query) return [];
|
|
323
|
+
const terms = query.split(" ").map((term) => `%${term}%`);
|
|
324
|
+
const clauses = terms.map(
|
|
325
|
+
() => "(normalized_text LIKE ? OR title LIKE ? OR summary LIKE ? OR facets LIKE ?)"
|
|
326
|
+
).join(" AND ");
|
|
327
|
+
const values = terms.flatMap((term) => [term, term, term, term]);
|
|
328
|
+
const result = await db.prepare(
|
|
329
|
+
`SELECT route_id
|
|
330
|
+
FROM content_search_index
|
|
331
|
+
WHERE model_id = ? AND ${clauses}
|
|
332
|
+
ORDER BY indexed_at DESC
|
|
333
|
+
LIMIT ?`
|
|
334
|
+
).bind(input.modelId, ...values, input.limit ?? 200).all();
|
|
335
|
+
return (result.results ?? []).map((row) => row.route_id).filter((routeId) => typeof routeId === "string");
|
|
336
|
+
}
|
|
337
|
+
async function filterItemsBySearchIndex(items, query, input) {
|
|
338
|
+
const normalized = normalizeSearchQuery(query);
|
|
339
|
+
if (!normalized) return [...items];
|
|
340
|
+
if (!input.getDatabase) return input.filterFallback(items, normalized);
|
|
341
|
+
try {
|
|
342
|
+
const db = await input.getDatabase();
|
|
343
|
+
if (!db) return input.filterFallback(items, normalized);
|
|
344
|
+
const routeIds = await querySearchIndexRouteIds(db, {
|
|
345
|
+
modelId: input.modelId,
|
|
346
|
+
query: normalized,
|
|
347
|
+
limit: Math.max(items.length, 200)
|
|
348
|
+
});
|
|
349
|
+
if (routeIds.length === 0) return input.filterFallback(items, normalized);
|
|
350
|
+
const order = routeOrder(items);
|
|
351
|
+
const matched = new Set(routeIds);
|
|
352
|
+
return items.filter((item) => matched.has(routeIdForItem(item))).sort(
|
|
353
|
+
(a, b) => (order.get(routeIdForItem(a)) ?? 0) - (order.get(routeIdForItem(b)) ?? 0)
|
|
354
|
+
);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.warn(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
tag: "content_search_index_error",
|
|
359
|
+
modelId: input.modelId,
|
|
360
|
+
message: error instanceof Error ? error.message : String(error)
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
return input.filterFallback(items, normalized);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function matchesIndexedItem(item, query) {
|
|
367
|
+
return matchesSearchQuery(indexValuesForItem(item), query);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/content/prewarm.ts
|
|
371
|
+
function logContentPrewarm(fields) {
|
|
372
|
+
try {
|
|
373
|
+
console.log(JSON.stringify({ tag: "content_prewarm", ...fields }));
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function prewarmPublicContentSearchIndex(targets, options) {
|
|
378
|
+
const startedAtDate = /* @__PURE__ */ new Date();
|
|
379
|
+
const startedAt = startedAtDate.toISOString();
|
|
380
|
+
const t0 = performance.now();
|
|
381
|
+
const requestedModels = new Set(options?.models?.filter(Boolean));
|
|
382
|
+
const selected = requestedModels.size > 0 ? targets.filter((target) => requestedModels.has(target.modelId)) : targets;
|
|
383
|
+
const models = [];
|
|
384
|
+
for (const target of selected) {
|
|
385
|
+
try {
|
|
386
|
+
const result = await target.run();
|
|
387
|
+
models.push({
|
|
388
|
+
modelId: target.modelId,
|
|
389
|
+
ok: true,
|
|
390
|
+
total: result.total,
|
|
391
|
+
indexed: result.indexed,
|
|
392
|
+
skipped: result.skipped
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
models.push({
|
|
396
|
+
modelId: target.modelId,
|
|
397
|
+
ok: false,
|
|
398
|
+
total: 0,
|
|
399
|
+
indexed: 0,
|
|
400
|
+
skipped: true,
|
|
401
|
+
error: error instanceof Error ? error.message : String(error)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
406
|
+
const output = {
|
|
407
|
+
ok: models.every((model) => model.ok),
|
|
408
|
+
startedAt,
|
|
409
|
+
finishedAt,
|
|
410
|
+
durationMs: Math.round((performance.now() - t0) * 100) / 100,
|
|
411
|
+
models
|
|
412
|
+
};
|
|
413
|
+
logContentPrewarm({
|
|
414
|
+
ok: output.ok,
|
|
415
|
+
started_at: output.startedAt,
|
|
416
|
+
finished_at: output.finishedAt,
|
|
417
|
+
duration_ms: output.durationMs,
|
|
418
|
+
models: output.models
|
|
419
|
+
});
|
|
420
|
+
return output;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/content/admin-summary.ts
|
|
424
|
+
function visibilityFor(model) {
|
|
425
|
+
if (model.visibility.public && model.visibility.admin) return "public+admin";
|
|
426
|
+
if (model.visibility.public) return "public";
|
|
427
|
+
if (model.visibility.admin) return "admin";
|
|
428
|
+
return "private";
|
|
429
|
+
}
|
|
430
|
+
function summarizeContentModelForAdmin(model) {
|
|
431
|
+
return {
|
|
432
|
+
id: model.id,
|
|
433
|
+
name: model.ui.name,
|
|
434
|
+
kind: model.kind,
|
|
435
|
+
visibility: visibilityFor(model),
|
|
436
|
+
listPath: model.routes.listPath,
|
|
437
|
+
detailPath: model.routes.detailPath,
|
|
438
|
+
publicApiPath: model.routes.publicApiPath,
|
|
439
|
+
dataSourceEnv: model.source.dataSourceEnv,
|
|
440
|
+
hasDefaultDataSource: Boolean(model.source.defaultDataSourceId),
|
|
441
|
+
fieldCount: Object.keys(model.source.fields).length,
|
|
442
|
+
capabilities: model.capabilities
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function getContentModelAdminSummaries(models = getRegisteredSources()) {
|
|
446
|
+
return models.map(summarizeContentModelForAdmin);
|
|
447
|
+
}
|
|
448
|
+
export {
|
|
449
|
+
authorizeContentRevalidate,
|
|
450
|
+
buildContentRevalidationPaths,
|
|
451
|
+
clearRegistryForTests,
|
|
452
|
+
defineContentSource,
|
|
453
|
+
deleteSearchIndexDocument,
|
|
454
|
+
deleteSearchIndexForModel,
|
|
455
|
+
filterItemsBySearchIndex,
|
|
456
|
+
filterMoviesBySearch,
|
|
457
|
+
filterPostsBySearch,
|
|
458
|
+
getContentModelAdminSummaries,
|
|
459
|
+
getMissingSearchIndexRouteIds,
|
|
460
|
+
getRegisteredSource,
|
|
461
|
+
getRegisteredSources,
|
|
462
|
+
matchesIndexedItem,
|
|
463
|
+
matchesSearchQuery,
|
|
464
|
+
normalizeSearchQuery,
|
|
465
|
+
previewContentModelInvalidation,
|
|
466
|
+
prewarmPublicContentSearchIndex,
|
|
467
|
+
querySearchIndexRouteIds,
|
|
468
|
+
readContentRevalidateRequest,
|
|
469
|
+
readContentRevalidateRequestFromUrl,
|
|
470
|
+
summarizeContentModelForAdmin,
|
|
471
|
+
upsertSearchIndexDocument
|
|
472
|
+
};
|
|
473
|
+
//# sourceMappingURL=index.js.map
|