@murumets-ee/content 0.96.0 → 0.97.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.
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-context-D8UXwWwr.mjs","names":[],"sources":["../src/paths/homepage.ts","../src/paths/site-scope.ts","../src/paths/resolve-context.ts"],"sourcesContent":["/**\n * Homepage pointer — the document that serves a site's `/` (plan/routing PR06,\n * D004; per site since plan `multisite` phase 03, D007).\n *\n * The value is the site registry row's `homeDocument` JSON (it replaced the\n * global `site.homeDocument` setting, which the registry migration copied onto\n * the `default` site; written by `setSiteHomepage` / `lumi site:set-home`). The\n * resolution layer parses it HERE, defensively (CLAUDE.md \"validate at the boundary\"):\n * an unset row OR a value that fails this schema means \"no homepage configured\"\n * (`/` resolves to `none`) — never a thrown error, since this runs on every `/`\n * request.\n *\n * This module is a LEAF (zod only) so the parse is unit-testable without a DB and\n * the resolution core stays free of settings/core imports; the actual\n * registry read is wired in `./resolve-context.ts`.\n */\n\nimport { z } from 'zod'\n\n/** A resolved pointer at the document that serves `/`. */\nexport interface HomepagePointer {\n entity: string\n id: string\n}\n\n/**\n * Shape of the `homeDocument` JSONB value. `id` is a UUID — entity row ids\n * are UUIDs across this codebase (only USER ids are varchar; CLAUDE.md).\n */\nexport const homepagePointerSchema = z.object({\n entity: z.string().min(1),\n id: z.string().uuid(),\n})\n\n/**\n * Parse a raw `homeDocument` value into a {@link HomepagePointer}, or `null`\n * when it's unset / malformed. Never throws.\n */\nexport function parseHomepagePointer(value: unknown): HomepagePointer | null {\n const parsed = homepagePointerSchema.safeParse(value)\n return parsed.success ? parsed.data : null\n}\n","/**\n * Which site a path-index / redirect access acts on (plan `multisite` phase 03).\n *\n * `toolkit_paths` and `toolkit_redirects` are partitioned by site: a client\n * cannot be built without one (`makeClient(db, site)`). This module is how\n * content's resolution doors get one without every caller threading it:\n *\n * - A {@link SiteSource} answers \"which site?\" lazily, AT CALL TIME — so a deps\n * bundle built once per app (the block resolver's, a menu resolver's) still\n * reads the site of the request that is running, never the site of whoever\n * built it first.\n * - {@link requestSiteSource} is the request's site: its explicit\n * `RequestContext.site`, else the single-site answer (D010), else a\n * `SiteRequiredError`. A multi-site app never gets a default.\n * - {@link pinnedSiteSource} is an explicit site (a per-site sitemap). It is a\n * narrowing only: inside a request already acting on ANOTHER site it throws,\n * because reading a second site from a request confined to the first is a\n * lateral move, not a narrowing.\n *\n * The readers built from a source ({@link sitePathsReader},\n * {@link siteRedirectsReader}) build a partitioned client per call — a\n * constructor, no I/O — so they are exactly as confined as the partition.\n */\n\nimport { getContext, requireRequestSite, type ToolkitApp } from '@murumets-ee/core'\nimport type { SecurityContext } from '@murumets-ee/entity'\nimport { isValidSiteHandle } from '@murumets-ee/entity'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { type ToolkitRedirectsClient, toolkitRedirectsTable } from '../redirects/schema.js'\nimport { type ToolkitPathsClient, toolkitPathsTable } from './schema.js'\n\n/** Resolves the site an access acts on — see the module docblock. */\nexport type SiteSource = () => Promise<string>\n\n/** The subject named in a `SiteRequiredError` from these doors. */\nconst SUBJECT = 'toolkit_paths'\n\n/** The request's site, resolved at call time. */\nexport function requestSiteSource(app?: ToolkitApp): SiteSource {\n return () => requireRequestSite(SUBJECT, app)\n}\n\n/** Assert `site` is a handle and that the ambient request is not confined to another site. */\nexport function assertPinnableSite(site: string): string {\n if (!isValidSiteHandle(site)) throw new Error(`'${String(site)}' is not a valid site handle`)\n const ambient = getContext()?.site\n if (ambient !== undefined && ambient !== site) {\n throw new Error(\n `A request acting on site '${ambient}' cannot read site '${site}' — a request is ` +\n 'confined to its site; start per-site work from a site-less frame.',\n )\n }\n return site\n}\n\n/** An explicit site — see the module docblock. Throws at CALL time on a lateral move. */\nexport function pinnedSiteSource(site: string): SiteSource {\n assertPinnableSite(site)\n return async () => assertPinnableSite(site)\n}\n\n/** `pinned` when given, else the request's site. */\nexport function siteSourceFor(app: ToolkitApp, pinned?: string): SiteSource {\n return pinned === undefined ? requestSiteSource(app) : pinnedSiteSource(pinned)\n}\n\n/**\n * A reach resolver confined to `site` — for the publish / noIndex checks behind\n * a pinned deps bundle, which must read the SAME site as the paths they check.\n * Carries the base resolver's checker (reach only) and nothing else of the\n * ambient request.\n */\nexport function pinReachResolver(base: () => SecurityContext, site: string): () => SecurityContext {\n return () => {\n assertPinnableSite(site)\n // An explicit `site` wins over any implicit-site resolver the base carries\n // (the data layer consults that only when no site is named).\n return Object.freeze({ ...base(), site })\n }\n}\n\n/** `toolkit_paths` reads for the source's site. */\nexport function sitePathsReader(\n db: PostgresJsDatabase,\n siteOf: SiteSource,\n): Pick<ToolkitPathsClient, 'findOne' | 'findMany'> {\n return {\n findOne: async (where) => toolkitPathsTable.makeClient(db, await siteOf()).findOne(where),\n findMany: async (options) => toolkitPathsTable.makeClient(db, await siteOf()).findMany(options),\n }\n}\n\n/** `toolkit_redirects` reads for the source's site. */\nexport function siteRedirectsReader(\n db: PostgresJsDatabase,\n siteOf: SiteSource,\n): Pick<ToolkitRedirectsClient, 'findOne' | 'findMany'> {\n return {\n findOne: async (where) => toolkitRedirectsTable.makeClient(db, await siteOf()).findOne(where),\n findMany: async (options) =>\n toolkitRedirectsTable.makeClient(db, await siteOf()).findMany(options),\n }\n}\n","/**\n * App-binding for the path-resolution core (plan/routing PR06).\n *\n * `./resolve.ts` is a LEAF — it takes every dependency (path/redirect readers,\n * publish checker, homepage reader) as injected params so it imports no\n * `@murumets-ee/core` graph. THIS module is the outer seam that holds those\n * imports and builds concrete `deps` from a live `ToolkitApp`. Only the\n * request-time resolution / enumeration surfaces (future admin routes,\n * content-api, a frontend catch-all) reach this module — never the `lumi` CLI\n * command graph (F027 guardrail; keeps the `server-only`-adjacent imports out of\n * `paths/jobs.ts`).\n *\n * **Publish check runs under an ELEVATED system context** (mirrors\n * `../locale-registry.ts`'s `SYSTEM_CONTEXT`), NOT the ambient request context.\n * Rationale + security: URL resolution is a SYSTEM derivation that must work for\n * any caller — an anonymous public request, or a build-time enumeration with no\n * request context at all — and must not fail just because the caller lacks a\n * per-entity `view` grant. The elevated context only bypasses the\n * permission/scope check; the PUBLISH FILTER (`QueryClient.buildPublishFilter`)\n * and the trashed-row exclusion still apply, so an unpublished / trashed\n * document still resolves to `null` → not routable.\n *\n * Since D017 that publish filter is CONDITIONAL — it holds for any principal\n * WITHOUT `view_unpublished`, and is lifted for one that holds it. What makes\n * the paragraph above true is therefore not the filter being unconditional but\n * {@link SYSTEM_CONTEXT} WITHHOLDING that action (and every write action with\n * it). No content data is returned from the check (only a boolean), so\n * elevation leaks nothing — the downstream content fetch still runs under the\n * caller's real permissions.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { createSystemReachResolver, readSiteRecord } from '@murumets-ee/core'\nimport {\n type Entity,\n getEntityTitleField,\n isSiteBound,\n type SecurityContext,\n} from '@murumets-ee/entity'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport { inArray } from 'drizzle-orm'\nimport type { PgColumn } from 'drizzle-orm/pg-core'\nimport { getContentConfig } from '../content-config-store.js'\nimport type { BreadcrumbTrailDeps } from './breadcrumbs.js'\nimport type { CanonicalPathsBatchDeps, EnumeratePathsDeps, PublishFilter } from './enumerate.js'\nimport { parseHomepagePointer } from './homepage.js'\nimport type { RedirectsListDeps } from './redirects-list.js'\nimport {\n type AlternatesDeps,\n type CanonicalPathDeps,\n createPublishChecker,\n type HomepageReader,\n type PublishChecker,\n type ResolvePathDeps,\n} from './resolve.js'\nimport {\n pinReachResolver,\n type SiteSource,\n sitePathsReader,\n siteRedirectsReader,\n siteSourceFor,\n} from './site-scope.js'\nimport type { SitemapDeps } from './sitemap.js'\n\n/**\n * Every builder below is SITE-CONFINED (plan `multisite` phase 03): its\n * `toolkit_paths` / `toolkit_redirects` readers, its publish checks and its\n * homepage reader all act on ONE site.\n *\n * - Omit `site` and that is the site of the request RUNNING the call (resolved\n * at call time, so a bundle built once per app still follows each request) —\n * its explicit `RequestContext.site`, else the single-site answer, else a\n * `SiteRequiredError`. This is every existing caller, unchanged.\n * - Pass `site` to pin one explicitly (a per-site sitemap or warmer run from a\n * site-less job). Pinning inside a request confined to another site throws.\n */\nexport interface SiteScopeOptions {\n site?: string\n}\n\n/**\n * Elevated **reach** resolver for the publish check — the roles/locale-registry\n * -read analog (see file header for why this is safe).\n *\n * It grants `view` and NOTHING else — in particular not `view_unpublished`,\n * which is what makes the file header's claim true. Until D017 the header's\n * \"the publish filter is applied by `QueryClient` regardless\" held for ANY\n * checker; now that publish-visibility IS a permission, a `checker: () => true`\n * here would answer \"yes\" to `view_unpublished` and every door this module backs\n * (`_resolve`, `_alternates`, `_paths`, sitemap, redirects) would start\n * resolving drafts to public URLs. {@link createSystemReachResolver} owns that\n * exception centrally so it can't be re-derived (or forgotten) per call site.\n *\n * The explicit id is the audit identity: it must be distinguishable from the\n * menu resolver's, or two elevated readers are one indistinguishable `'system'`\n * in the log.\n */\nconst SYSTEM_CONTEXT: () => SecurityContext = createSystemReachResolver('path-resolver')\n\n/** {@link SYSTEM_CONTEXT}, confined to `site` when one is pinned. */\nfunction systemContextFor(site: string | undefined): () => SecurityContext {\n return site === undefined ? SYSTEM_CONTEXT : pinReachResolver(SYSTEM_CONTEXT, site)\n}\n\n/**\n * Build the minimal deps for {@link import('./resolve.js').getCanonicalPath} —\n * just the `toolkit_paths` reader. Task 2's `resolveEntityHref` / search\n * url-builder re-points use this (no publish checker / settings machinery).\n */\nexport function buildCanonicalPathDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): CanonicalPathDeps {\n return { pathsClient: sitePathsReader(app.db.readOnly, siteSourceFor(app, options.site)) }\n}\n\n/**\n * Build a live {@link PublishChecker} that reads through an elevated-context\n * `QueryClient` per entity (publish filter auto-applied). See file header.\n */\nexport function buildPublishChecker(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): PublishChecker {\n const contextResolver = systemContextFor(options.site)\n return createPublishChecker({\n entityResolver: (name) => app.entities.get(name),\n queryClientFactory: (entity: Entity) =>\n new QueryClient({\n entity,\n db: app.db.readOnly,\n logger: app.logger.child({ resolve: 'publish-check', entity: entity.name }),\n contextResolver,\n }),\n })\n}\n\n/**\n * Build the homepage reader for a site (plan `multisite` D007): the site\n * registry row's `homeDocument` (`readSiteRecord` — read with reach, cached\n * briefly), zod-parsed defensively (unset / malformed → `null`, never a throw\n * — this runs on every `/` request).\n *\n * The pointer is only as good as the resolution that follows it: `/` publish-\n * checks the document through a client confined to the SAME site, so a pointer\n * at another site's document (legacy data, a hand-edited row) resolves to\n * `none` instead of serving it (D006, defence in depth — writes are refused\n * by `setSiteHomepage`).\n */\nexport function createHomepageReader(app: ToolkitApp, siteOf: SiteSource): HomepageReader {\n return async () => {\n const record = await readSiteRecord(app, await siteOf())\n const pointer = record ? parseHomepagePointer(record.homeDocument) : null\n if (!pointer) return null\n // Only a routable document may serve `/` — and on an app serving several\n // sites only a site-bound one (a shared row belongs to no site's `/`).\n const entity = app.entities.get(pointer.entity)\n if (!entity?.frontend?.path) return null\n if (app.config.sites?.multiple === true && !isSiteBound(entity)) return null\n return pointer\n }\n}\n\n/**\n * Build the deps for {@link import('./enumerate.js').enumeratePaths} — the\n * `toolkit_paths` reader plus a BATCHED publish filter. `filterPublished` runs\n * ONE elevated-context `QueryClient.findMany({ where: inArray(getTable().id,\n * ids), locale })` per entity-type group (not per row) — the trash filter\n * applies unconditionally, and the publish filter applies because the elevated\n * context WITHHOLDS `view_unpublished` (D017; see file header), so no\n * unpublished/trashed id is returned. An unknown/unregistered entity type yields the empty set (its\n * rows are treated as unpublished → dropped, fail-closed).\n */\nexport function buildEnumerateDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): EnumeratePathsDeps {\n const db = app.db.readOnly\n const contextResolver = systemContextFor(options.site)\n return {\n pathsClient: sitePathsReader(db, siteSourceFor(app, options.site)),\n filterPublished: async (entityType, entityIds, locale) => {\n if (entityIds.length === 0) return new Set()\n const entity = app.entities.get(entityType)\n if (entity === undefined) return new Set()\n const client = new QueryClient({\n entity,\n db,\n logger: app.logger.child({ resolve: 'enumerate-publish', entity: entity.name }),\n contextResolver,\n })\n // Dynamic `id` column ref for the batched IN filter. `getTable()` is\n // `PgTableWithColumns<any>`; the structural cast narrows to the one column\n // we filter on (the same idiom paths/jobs.ts uses for dynamic column refs).\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n const rows = await client.findMany({ where: inArray(idColumn, [...entityIds]), locale })\n const published = new Set<string>()\n for (const row of rows) {\n const id = (row as { id?: unknown }).id\n if (typeof id === 'string') published.add(id)\n }\n return published\n },\n }\n}\n\n/**\n * Build the deps for {@link import('./enumerate.js').getCanonicalPaths} — just\n * the `toolkit_paths` reader (batched, NO publish filter, like\n * {@link buildCanonicalPathDeps}). Task 2's search url re-point uses this to\n * resolve a search page's result urls from the index in one query.\n */\nexport function buildCanonicalPathsBatchDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): CanonicalPathsBatchDeps {\n return { pathsClient: sitePathsReader(app.db.readOnly, siteSourceFor(app, options.site)) }\n}\n\n/**\n * Build the `filterNoIndex` dep for {@link import('./sitemap.js').generateSitemap}\n * (PR 11's `seoMeta()`) — mirrors {@link buildEnumerateDeps}'s `filterPublished`\n * construction EXACTLY (elevated-context `QueryClient`, one `findMany` per\n * entity-type group), with two differences:\n *\n * 1. Short-circuits to the empty set when the entity does NOT declare\n * `seoMeta()` — most routable entities won't have this behavior, and\n * querying a `noIndex` column that doesn't exist would throw.\n * 2. Filters on the LOCALE-MERGED DTO rather than a raw SQL column, because\n * `noIndex` is `translatable: true` — its live per-locale value only\n * exists after `QueryClient.findMany`'s `mergeTranslations` step, not as a\n * column on the main table itself (unlike `filterPublished`'s `status`,\n * which is never translatable). The `id IN (...)` filter is still pushed\n * to SQL; only the `noIndex` check runs over the (batch-bounded) merged\n * rows in memory.\n */\nfunction buildNoIndexFilter(app: ToolkitApp, options: SiteScopeOptions = {}): PublishFilter {\n const db = app.db.readOnly\n const contextResolver = systemContextFor(options.site)\n return async (entityType, entityIds, locale) => {\n if (entityIds.length === 0) return new Set()\n const entity = app.entities.get(entityType)\n if (entity === undefined) return new Set()\n if (!entity.behaviors?.some((b) => b.name === 'seoMeta')) return new Set()\n const client = new QueryClient({\n entity,\n db,\n logger: app.logger.child({ resolve: 'sitemap-noindex', entity: entity.name }),\n contextResolver,\n })\n // Dynamic `id` column ref for the batched IN filter — same idiom as\n // `buildEnumerateDeps.filterPublished`.\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n const rows = await client.findMany({ where: inArray(idColumn, [...entityIds]), locale })\n const noIndexed = new Set<string>()\n for (const row of rows) {\n const id = (row as { id?: unknown }).id\n const noIndex = (row as { noIndex?: unknown }).noIndex\n if (typeof id === 'string' && noIndex === true) noIndexed.add(id)\n }\n return noIndexed\n }\n}\n\n/**\n * Build the deps for {@link import('./sitemap.js').generateSitemap} — the\n * one-stop bundle of {@link buildEnumerateDeps} (per-locale enumeration + its\n * BATCHED publish filter, reused for the cross-locale alternate publish gate),\n * {@link buildCanonicalPathsBatchDeps} (the batched cross-locale forward index\n * read), and {@link buildNoIndexFilter} (PR 11's batched `noIndex` exclusion).\n * Mirrors `buildResolveDeps`'s one-stop-deps pattern.\n */\nexport function buildSitemapDeps(app: ToolkitApp, options: SiteScopeOptions = {}): SitemapDeps {\n return {\n enumerate: buildEnumerateDeps(app, options),\n canonicalBatch: buildCanonicalPathsBatchDeps(app, options),\n filterNoIndex: buildNoIndexFilter(app, options),\n }\n}\n\n/**\n * Build the deps for {@link import('./redirects-list.js').getRedirects} — the\n * one-stop bundle for the build-time redirect list (PR08). Composes\n * {@link buildEnumerateDeps} (its `toolkit_paths` reader powers both the\n * historical keyset and the `path`-kind existence check; its BATCHED\n * `filterPublished` is the shared publish gate), {@link buildCanonicalPathsBatchDeps}\n * (the batched owning-document current-canonical lookup), and a\n * `toolkit_redirects` reader for the manual-redirect keyset. Mirrors\n * {@link buildSitemapDeps}'s one-stop-deps style.\n */\nexport function buildRedirectsListDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): RedirectsListDeps {\n const enumerate = buildEnumerateDeps(app, options)\n return {\n redirectsClient: siteRedirectsReader(app.db.readOnly, siteSourceFor(app, options.site)),\n pathsClient: enumerate.pathsClient,\n canonicalBatch: buildCanonicalPathsBatchDeps(app, options),\n filterPublished: enumerate.filterPublished,\n }\n}\n\n/**\n * Build the full {@link ResolvePathDeps} (+ {@link AlternatesDeps}) from an app —\n * the one-stop deps for `resolvePath` / `resolveDraftPath` / `getAlternates`.\n * `locales` are the statically-configured content locales (v1 — matches the\n * redirect routes' own configured-locale source; DB-merged locale overrides are\n * a later refinement).\n */\nexport function buildResolveDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): ResolvePathDeps & AlternatesDeps {\n const db = app.db.readOnly\n const siteOf = siteSourceFor(app, options.site)\n return {\n pathsClient: sitePathsReader(db, siteOf),\n redirectsClient: siteRedirectsReader(db, siteOf),\n publishChecker: buildPublishChecker(app, options),\n homepageReader: createHomepageReader(app, siteOf),\n locales: getContentConfig().locales.map((l) => l.code),\n // Only consumer: the historical-redirect lookup's truncation warning.\n logger: app.logger,\n }\n}\n\n/**\n * Build the deps for {@link import('./breadcrumbs.js').resolveBreadcrumbTrail}:\n * the site-confined `toolkit_paths` reader and a publish-gated TITLE read — the\n * same elevated REACH read `buildEnumerateDeps.filterPublished` makes (it\n * withholds `view_unpublished`, so an unpublished or trashed ancestor comes\n * back as nothing), projecting the entity's title column instead of just the id.\n */\nexport function buildBreadcrumbDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): BreadcrumbTrailDeps {\n const db = app.db.readOnly\n const contextResolver = systemContextFor(options.site)\n return {\n pathsClient: sitePathsReader(db, siteSourceFor(app, options.site)),\n publishedTitles: async (entityType, ids, locale) => {\n const titles = new Map<string, string>()\n if (ids.length === 0) return titles\n const entity = app.entities.get(entityType)\n if (entity === undefined) return titles\n const titleField = getEntityTitleField(entity)\n // A uuid is not a title: an entity with no title column is no breadcrumb.\n if (titleField === 'id') return titles\n const client = new QueryClient({\n entity,\n db,\n logger: app.logger.child({ resolve: 'breadcrumb-titles', entity: entity.name }),\n contextResolver,\n })\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n const rows = await client.findMany({\n where: inArray(idColumn, [...ids]),\n locale,\n limit: ids.length,\n })\n for (const row of rows) {\n const record = row as Record<string, unknown>\n const id = record.id\n const title = record[titleField]\n if (typeof id === 'string' && typeof title === 'string' && title.trim() !== '') {\n titles.set(id, title.trim())\n }\n }\n return titles\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resolve-context-DklN53BH.mjs","names":[],"sources":["../src/paths/homepage.ts","../src/paths/site-scope.ts","../src/paths/resolve-context.ts"],"sourcesContent":["/**\n * Homepage pointer — the document that serves a site's `/` (plan/routing PR06,\n * D004; per site since plan `multisite` phase 03, D007).\n *\n * The value is the site registry row's `homeDocument` JSON (it replaced the\n * global `site.homeDocument` setting, which the registry migration copied onto\n * the `default` site; written by `setSiteHomepage` / `lumi site:set-home`). The\n * resolution layer parses it HERE, defensively (CLAUDE.md \"validate at the boundary\"):\n * an unset row OR a value that fails this schema means \"no homepage configured\"\n * (`/` resolves to `none`) — never a thrown error, since this runs on every `/`\n * request.\n *\n * This module is a LEAF (zod only) so the parse is unit-testable without a DB and\n * the resolution core stays free of settings/core imports; the actual\n * registry read is wired in `./resolve-context.ts`.\n */\n\nimport { z } from 'zod'\n\n/** A resolved pointer at the document that serves `/`. */\nexport interface HomepagePointer {\n entity: string\n id: string\n}\n\n/**\n * Shape of the `homeDocument` JSONB value. `id` is a UUID — entity row ids\n * are UUIDs across this codebase (only USER ids are varchar; CLAUDE.md).\n */\nexport const homepagePointerSchema = z.object({\n entity: z.string().min(1),\n id: z.string().uuid(),\n})\n\n/**\n * Parse a raw `homeDocument` value into a {@link HomepagePointer}, or `null`\n * when it's unset / malformed. Never throws.\n */\nexport function parseHomepagePointer(value: unknown): HomepagePointer | null {\n const parsed = homepagePointerSchema.safeParse(value)\n return parsed.success ? parsed.data : null\n}\n","/**\n * Which site a path-index / redirect access acts on (plan `multisite` phase 03).\n *\n * `toolkit_paths` and `toolkit_redirects` are partitioned by site: a client\n * cannot be built without one (`makeClient(db, site)`). This module is how\n * content's resolution doors get one without every caller threading it:\n *\n * - A {@link SiteSource} answers \"which site?\" lazily, AT CALL TIME — so a deps\n * bundle built once per app (the block resolver's, a menu resolver's) still\n * reads the site of the request that is running, never the site of whoever\n * built it first.\n * - {@link requestSiteSource} is the request's site: its explicit\n * `RequestContext.site`, else the single-site answer (D010), else a\n * `SiteRequiredError`. A multi-site app never gets a default.\n * - {@link pinnedSiteSource} is an explicit site (a per-site sitemap). It is a\n * narrowing only: inside a request already acting on ANOTHER site it throws,\n * because reading a second site from a request confined to the first is a\n * lateral move, not a narrowing.\n *\n * The readers built from a source ({@link sitePathsReader},\n * {@link siteRedirectsReader}) build a partitioned client per call — a\n * constructor, no I/O — so they are exactly as confined as the partition.\n */\n\nimport { getContext, requireRequestSite, type ToolkitApp } from '@murumets-ee/core'\nimport type { SecurityContext } from '@murumets-ee/entity'\nimport { isValidSiteHandle } from '@murumets-ee/entity'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { type ToolkitRedirectsClient, toolkitRedirectsTable } from '../redirects/schema.js'\nimport { type ToolkitPathsClient, toolkitPathsTable } from './schema.js'\n\n/** Resolves the site an access acts on — see the module docblock. */\nexport type SiteSource = () => Promise<string>\n\n/** The subject named in a `SiteRequiredError` from these doors. */\nconst SUBJECT = 'toolkit_paths'\n\n/** The request's site, resolved at call time. */\nexport function requestSiteSource(app?: ToolkitApp): SiteSource {\n return () => requireRequestSite(SUBJECT, app)\n}\n\n/** Assert `site` is a handle and that the ambient request is not confined to another site. */\nexport function assertPinnableSite(site: string): string {\n if (!isValidSiteHandle(site)) throw new Error(`'${String(site)}' is not a valid site handle`)\n const ambient = getContext()?.site\n if (ambient !== undefined && ambient !== site) {\n throw new Error(\n `A request acting on site '${ambient}' cannot read site '${site}' — a request is ` +\n 'confined to its site; start per-site work from a site-less frame.',\n )\n }\n return site\n}\n\n/** An explicit site — see the module docblock. Throws at CALL time on a lateral move. */\nexport function pinnedSiteSource(site: string): SiteSource {\n assertPinnableSite(site)\n return async () => assertPinnableSite(site)\n}\n\n/** `pinned` when given, else the request's site. */\nexport function siteSourceFor(app: ToolkitApp, pinned?: string): SiteSource {\n return pinned === undefined ? requestSiteSource(app) : pinnedSiteSource(pinned)\n}\n\n/**\n * A reach resolver confined to `site` — for the publish / noIndex checks behind\n * a pinned deps bundle, which must read the SAME site as the paths they check.\n * Carries the base resolver's checker (reach only) and nothing else of the\n * ambient request.\n */\nexport function pinReachResolver(base: () => SecurityContext, site: string): () => SecurityContext {\n return () => {\n assertPinnableSite(site)\n // An explicit `site` wins over any implicit-site resolver the base carries\n // (the data layer consults that only when no site is named).\n return Object.freeze({ ...base(), site })\n }\n}\n\n/** `toolkit_paths` reads for the source's site. */\nexport function sitePathsReader(\n db: PostgresJsDatabase,\n siteOf: SiteSource,\n): Pick<ToolkitPathsClient, 'findOne' | 'findMany'> {\n return {\n findOne: async (where) => toolkitPathsTable.makeClient(db, await siteOf()).findOne(where),\n findMany: async (options) => toolkitPathsTable.makeClient(db, await siteOf()).findMany(options),\n }\n}\n\n/** `toolkit_redirects` reads for the source's site. */\nexport function siteRedirectsReader(\n db: PostgresJsDatabase,\n siteOf: SiteSource,\n): Pick<ToolkitRedirectsClient, 'findOne' | 'findMany'> {\n return {\n findOne: async (where) => toolkitRedirectsTable.makeClient(db, await siteOf()).findOne(where),\n findMany: async (options) =>\n toolkitRedirectsTable.makeClient(db, await siteOf()).findMany(options),\n }\n}\n","/**\n * App-binding for the path-resolution core (plan/routing PR06).\n *\n * `./resolve.ts` is a LEAF — it takes every dependency (path/redirect readers,\n * publish checker, homepage reader) as injected params so it imports no\n * `@murumets-ee/core` graph. THIS module is the outer seam that holds those\n * imports and builds concrete `deps` from a live `ToolkitApp`. Only the\n * request-time resolution / enumeration surfaces (future admin routes,\n * content-api, a frontend catch-all) reach this module — never the `lumi` CLI\n * command graph (F027 guardrail; keeps the `server-only`-adjacent imports out of\n * `paths/jobs.ts`).\n *\n * **Publish check runs under an ELEVATED system context** (mirrors\n * `../locale-registry.ts`'s `SYSTEM_CONTEXT`), NOT the ambient request context.\n * Rationale + security: URL resolution is a SYSTEM derivation that must work for\n * any caller — an anonymous public request, or a build-time enumeration with no\n * request context at all — and must not fail just because the caller lacks a\n * per-entity `view` grant. The elevated context only bypasses the\n * permission/scope check; the PUBLISH FILTER (`QueryClient.buildPublishFilter`)\n * and the trashed-row exclusion still apply, so an unpublished / trashed\n * document still resolves to `null` → not routable.\n *\n * Since D017 that publish filter is CONDITIONAL — it holds for any principal\n * WITHOUT `view_unpublished`, and is lifted for one that holds it. What makes\n * the paragraph above true is therefore not the filter being unconditional but\n * {@link SYSTEM_CONTEXT} WITHHOLDING that action (and every write action with\n * it). No content data is returned from the check (only a boolean), so\n * elevation leaks nothing — the downstream content fetch still runs under the\n * caller's real permissions.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { createSystemReachResolver, readSiteRecord } from '@murumets-ee/core'\nimport {\n type Entity,\n getEntityTitleField,\n isSiteBound,\n type SecurityContext,\n} from '@murumets-ee/entity'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport { inArray } from 'drizzle-orm'\nimport type { PgColumn } from 'drizzle-orm/pg-core'\nimport { getContentConfig } from '../content-config-store.js'\nimport type { BreadcrumbTrailDeps } from './breadcrumbs.js'\nimport type { CanonicalPathsBatchDeps, EnumeratePathsDeps, PublishFilter } from './enumerate.js'\nimport { parseHomepagePointer } from './homepage.js'\nimport type { RedirectsListDeps } from './redirects-list.js'\nimport {\n type AlternatesDeps,\n type CanonicalPathDeps,\n createPublishChecker,\n type HomepageReader,\n type PublishChecker,\n type ResolvePathDeps,\n} from './resolve.js'\nimport {\n pinReachResolver,\n type SiteSource,\n sitePathsReader,\n siteRedirectsReader,\n siteSourceFor,\n} from './site-scope.js'\nimport type { SitemapDeps } from './sitemap.js'\n\n/**\n * Every builder below is SITE-CONFINED (plan `multisite` phase 03): its\n * `toolkit_paths` / `toolkit_redirects` readers, its publish checks and its\n * homepage reader all act on ONE site.\n *\n * - Omit `site` and that is the site of the request RUNNING the call (resolved\n * at call time, so a bundle built once per app still follows each request) —\n * its explicit `RequestContext.site`, else the single-site answer, else a\n * `SiteRequiredError`. This is every existing caller, unchanged.\n * - Pass `site` to pin one explicitly (a per-site sitemap or warmer run from a\n * site-less job). Pinning inside a request confined to another site throws.\n */\nexport interface SiteScopeOptions {\n site?: string\n}\n\n/**\n * Elevated **reach** resolver for the publish check — the roles/locale-registry\n * -read analog (see file header for why this is safe).\n *\n * It grants `view` and NOTHING else — in particular not `view_unpublished`,\n * which is what makes the file header's claim true. Until D017 the header's\n * \"the publish filter is applied by `QueryClient` regardless\" held for ANY\n * checker; now that publish-visibility IS a permission, a `checker: () => true`\n * here would answer \"yes\" to `view_unpublished` and every door this module backs\n * (`_resolve`, `_alternates`, `_paths`, sitemap, redirects) would start\n * resolving drafts to public URLs. {@link createSystemReachResolver} owns that\n * exception centrally so it can't be re-derived (or forgotten) per call site.\n *\n * The explicit id is the audit identity: it must be distinguishable from the\n * menu resolver's, or two elevated readers are one indistinguishable `'system'`\n * in the log.\n */\nconst SYSTEM_CONTEXT: () => SecurityContext = createSystemReachResolver('path-resolver')\n\n/** {@link SYSTEM_CONTEXT}, confined to `site` when one is pinned. */\nfunction systemContextFor(site: string | undefined): () => SecurityContext {\n return site === undefined ? SYSTEM_CONTEXT : pinReachResolver(SYSTEM_CONTEXT, site)\n}\n\n/**\n * Build the minimal deps for {@link import('./resolve.js').getCanonicalPath} —\n * just the `toolkit_paths` reader. Task 2's `resolveEntityHref` / search\n * url-builder re-points use this (no publish checker / settings machinery).\n */\nexport function buildCanonicalPathDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): CanonicalPathDeps {\n return { pathsClient: sitePathsReader(app.db.readOnly, siteSourceFor(app, options.site)) }\n}\n\n/**\n * Build a live {@link PublishChecker} that reads through an elevated-context\n * `QueryClient` per entity (publish filter auto-applied). See file header.\n */\nexport function buildPublishChecker(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): PublishChecker {\n const contextResolver = systemContextFor(options.site)\n return createPublishChecker({\n entityResolver: (name) => app.entities.get(name),\n queryClientFactory: (entity: Entity) =>\n new QueryClient({\n entity,\n db: app.db.readOnly,\n logger: app.logger.child({ resolve: 'publish-check', entity: entity.name }),\n contextResolver,\n }),\n })\n}\n\n/**\n * Build the homepage reader for a site (plan `multisite` D007): the site\n * registry row's `homeDocument` (`readSiteRecord` — read with reach, cached\n * briefly), zod-parsed defensively (unset / malformed → `null`, never a throw\n * — this runs on every `/` request).\n *\n * The pointer is only as good as the resolution that follows it: `/` publish-\n * checks the document through a client confined to the SAME site, so a pointer\n * at another site's document (legacy data, a hand-edited row) resolves to\n * `none` instead of serving it (D006, defence in depth — writes are refused\n * by `setSiteHomepage`).\n */\nexport function createHomepageReader(app: ToolkitApp, siteOf: SiteSource): HomepageReader {\n return async () => {\n const record = await readSiteRecord(app, await siteOf())\n const pointer = record ? parseHomepagePointer(record.homeDocument) : null\n if (!pointer) return null\n // Only a routable document may serve `/` — and on an app serving several\n // sites only a site-bound one (a shared row belongs to no site's `/`).\n const entity = app.entities.get(pointer.entity)\n if (!entity?.frontend?.path) return null\n if (app.config.sites?.multiple === true && !isSiteBound(entity)) return null\n return pointer\n }\n}\n\n/**\n * Build the deps for {@link import('./enumerate.js').enumeratePaths} — the\n * `toolkit_paths` reader plus a BATCHED publish filter. `filterPublished` runs\n * ONE elevated-context `QueryClient.findMany({ where: inArray(getTable().id,\n * ids), locale })` per entity-type group (not per row) — the trash filter\n * applies unconditionally, and the publish filter applies because the elevated\n * context WITHHOLDS `view_unpublished` (D017; see file header), so no\n * unpublished/trashed id is returned. An unknown/unregistered entity type yields the empty set (its\n * rows are treated as unpublished → dropped, fail-closed).\n */\nexport function buildEnumerateDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): EnumeratePathsDeps {\n const db = app.db.readOnly\n const contextResolver = systemContextFor(options.site)\n return {\n pathsClient: sitePathsReader(db, siteSourceFor(app, options.site)),\n filterPublished: async (entityType, entityIds, locale) => {\n if (entityIds.length === 0) return new Set()\n const entity = app.entities.get(entityType)\n if (entity === undefined) return new Set()\n const client = new QueryClient({\n entity,\n db,\n logger: app.logger.child({ resolve: 'enumerate-publish', entity: entity.name }),\n contextResolver,\n })\n // Dynamic `id` column ref for the batched IN filter. `getTable()` is\n // `PgTableWithColumns<any>`; the structural cast narrows to the one column\n // we filter on (the same idiom paths/jobs.ts uses for dynamic column refs).\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n const rows = await client.findMany({ where: inArray(idColumn, [...entityIds]), locale })\n const published = new Set<string>()\n for (const row of rows) {\n const id = (row as { id?: unknown }).id\n if (typeof id === 'string') published.add(id)\n }\n return published\n },\n }\n}\n\n/**\n * Build the deps for {@link import('./enumerate.js').getCanonicalPaths} — just\n * the `toolkit_paths` reader (batched, NO publish filter, like\n * {@link buildCanonicalPathDeps}). Task 2's search url re-point uses this to\n * resolve a search page's result urls from the index in one query.\n */\nexport function buildCanonicalPathsBatchDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): CanonicalPathsBatchDeps {\n return { pathsClient: sitePathsReader(app.db.readOnly, siteSourceFor(app, options.site)) }\n}\n\n/**\n * Build the `filterNoIndex` dep for {@link import('./sitemap.js').generateSitemap}\n * (PR 11's `seoMeta()`) — mirrors {@link buildEnumerateDeps}'s `filterPublished`\n * construction EXACTLY (elevated-context `QueryClient`, one `findMany` per\n * entity-type group), with two differences:\n *\n * 1. Short-circuits to the empty set when the entity does NOT declare\n * `seoMeta()` — most routable entities won't have this behavior, and\n * querying a `noIndex` column that doesn't exist would throw.\n * 2. Filters on the LOCALE-MERGED DTO rather than a raw SQL column, because\n * `noIndex` is `translatable: true` — its live per-locale value only\n * exists after `QueryClient.findMany`'s `mergeTranslations` step, not as a\n * column on the main table itself (unlike `filterPublished`'s `status`,\n * which is never translatable). The `id IN (...)` filter is still pushed\n * to SQL; only the `noIndex` check runs over the (batch-bounded) merged\n * rows in memory.\n */\nfunction buildNoIndexFilter(app: ToolkitApp, options: SiteScopeOptions = {}): PublishFilter {\n const db = app.db.readOnly\n const contextResolver = systemContextFor(options.site)\n return async (entityType, entityIds, locale) => {\n if (entityIds.length === 0) return new Set()\n const entity = app.entities.get(entityType)\n if (entity === undefined) return new Set()\n if (!entity.behaviors?.some((b) => b.name === 'seoMeta')) return new Set()\n const client = new QueryClient({\n entity,\n db,\n logger: app.logger.child({ resolve: 'sitemap-noindex', entity: entity.name }),\n contextResolver,\n })\n // Dynamic `id` column ref for the batched IN filter — same idiom as\n // `buildEnumerateDeps.filterPublished`.\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n const rows = await client.findMany({ where: inArray(idColumn, [...entityIds]), locale })\n const noIndexed = new Set<string>()\n for (const row of rows) {\n const id = (row as { id?: unknown }).id\n const noIndex = (row as { noIndex?: unknown }).noIndex\n if (typeof id === 'string' && noIndex === true) noIndexed.add(id)\n }\n return noIndexed\n }\n}\n\n/**\n * Build the deps for {@link import('./sitemap.js').generateSitemap} — the\n * one-stop bundle of {@link buildEnumerateDeps} (per-locale enumeration + its\n * BATCHED publish filter, reused for the cross-locale alternate publish gate),\n * {@link buildCanonicalPathsBatchDeps} (the batched cross-locale forward index\n * read), and {@link buildNoIndexFilter} (PR 11's batched `noIndex` exclusion).\n * Mirrors `buildResolveDeps`'s one-stop-deps pattern.\n */\nexport function buildSitemapDeps(app: ToolkitApp, options: SiteScopeOptions = {}): SitemapDeps {\n return {\n enumerate: buildEnumerateDeps(app, options),\n canonicalBatch: buildCanonicalPathsBatchDeps(app, options),\n filterNoIndex: buildNoIndexFilter(app, options),\n }\n}\n\n/**\n * Build the deps for {@link import('./redirects-list.js').getRedirects} — the\n * one-stop bundle for the build-time redirect list (PR08). Composes\n * {@link buildEnumerateDeps} (its `toolkit_paths` reader powers both the\n * historical keyset and the `path`-kind existence check; its BATCHED\n * `filterPublished` is the shared publish gate), {@link buildCanonicalPathsBatchDeps}\n * (the batched owning-document current-canonical lookup), and a\n * `toolkit_redirects` reader for the manual-redirect keyset. Mirrors\n * {@link buildSitemapDeps}'s one-stop-deps style.\n */\nexport function buildRedirectsListDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): RedirectsListDeps {\n const enumerate = buildEnumerateDeps(app, options)\n return {\n redirectsClient: siteRedirectsReader(app.db.readOnly, siteSourceFor(app, options.site)),\n pathsClient: enumerate.pathsClient,\n canonicalBatch: buildCanonicalPathsBatchDeps(app, options),\n filterPublished: enumerate.filterPublished,\n }\n}\n\n/**\n * Build the full {@link ResolvePathDeps} (+ {@link AlternatesDeps}) from an app —\n * the one-stop deps for `resolvePath` / `resolveDraftPath` / `getAlternates`.\n * `locales` are the statically-configured content locales (v1 — matches the\n * redirect routes' own configured-locale source; DB-merged locale overrides are\n * a later refinement).\n */\nexport function buildResolveDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): ResolvePathDeps & AlternatesDeps {\n const db = app.db.readOnly\n const siteOf = siteSourceFor(app, options.site)\n return {\n pathsClient: sitePathsReader(db, siteOf),\n redirectsClient: siteRedirectsReader(db, siteOf),\n publishChecker: buildPublishChecker(app, options),\n homepageReader: createHomepageReader(app, siteOf),\n // Truthiness, matching the writers' gate (jobs.ts `targetEntities`).\n isRoutable: (entityType) => !!app.entities.get(entityType)?.frontend?.path,\n locales: getContentConfig().locales.map((l) => l.code),\n // Only consumer: the historical-redirect lookup's truncation warning.\n logger: app.logger,\n }\n}\n\n/**\n * Build the deps for {@link import('./breadcrumbs.js').resolveBreadcrumbTrail}:\n * the site-confined `toolkit_paths` reader and a publish-gated TITLE read — the\n * same elevated REACH read `buildEnumerateDeps.filterPublished` makes (it\n * withholds `view_unpublished`, so an unpublished or trashed ancestor comes\n * back as nothing), projecting the entity's title column instead of just the id.\n */\nexport function buildBreadcrumbDeps(\n app: ToolkitApp,\n options: SiteScopeOptions = {},\n): BreadcrumbTrailDeps {\n const db = app.db.readOnly\n const contextResolver = systemContextFor(options.site)\n return {\n pathsClient: sitePathsReader(db, siteSourceFor(app, options.site)),\n publishedTitles: async (entityType, ids, locale) => {\n const titles = new Map<string, string>()\n if (ids.length === 0) return titles\n const entity = app.entities.get(entityType)\n if (entity === undefined) return titles\n const titleField = getEntityTitleField(entity)\n // A uuid is not a title: an entity with no title column is no breadcrumb.\n if (titleField === 'id') return titles\n const client = new QueryClient({\n entity,\n db,\n logger: app.logger.child({ resolve: 'breadcrumb-titles', entity: entity.name }),\n contextResolver,\n })\n const idColumn = (client.getTable() as unknown as { id: PgColumn }).id\n const rows = await client.findMany({\n where: inArray(idColumn, [...ids]),\n locale,\n limit: ids.length,\n })\n for (const row of rows) {\n const record = row as Record<string, unknown>\n const id = record.id\n const title = record[titleField]\n if (typeof id === 'string' && typeof title === 'string' && title.trim() !== '') {\n titles.set(id, title.trim())\n }\n }\n return titles\n },\n }\n}\n"],"mappings":"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"}
@@ -158,6 +158,15 @@ interface ResolvePathDeps extends CanonicalPathDeps {
158
158
  redirectsClient: RedirectLookupReader;
159
159
  publishChecker: PublishChecker;
160
160
  homepageReader: HomepageReader;
161
+ /**
162
+ * Is this entity type still routable (does it declare `frontend.path`)? A
163
+ * `toolkit_paths` row outlives its entity losing the path (F068), so every
164
+ * tier that serves a row's document asks first: a row of a non-routable
165
+ * entity is `none`, never a document or a redirect. `buildResolveDeps`
166
+ * always supplies it; omitted (unit-test builders only) means "every entity
167
+ * is routable".
168
+ */
169
+ isRoutable?: (entityType: string) => boolean;
161
170
  /**
162
171
  * Optional so the unit-test deps builders stay unchanged. Its only use is
163
172
  * letting the historical-redirect lookup report a truncated candidate window
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.mts","names":[],"sources":["../src/redirects/target.ts","../src/paths/index-reader.ts","../src/paths/homepage.ts","../src/paths/resolve.ts","../src/paths/enumerate.ts","../src/paths/sitemap.ts","../src/paths/site-sitemap.ts","../src/paths/redirects-list.ts","../src/paths/site-scope.ts","../src/paths/resolve-context.ts"],"mappings":"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"}
1
+ {"version":3,"file":"resolve.d.mts","names":[],"sources":["../src/redirects/target.ts","../src/paths/index-reader.ts","../src/paths/homepage.ts","../src/paths/resolve.ts","../src/paths/enumerate.ts","../src/paths/sitemap.ts","../src/paths/site-sitemap.ts","../src/paths/redirects-list.ts","../src/paths/site-scope.ts","../src/paths/resolve-context.ts"],"mappings":"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"}
package/dist/resolve.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import{i as e,t}from"./locale-registry-Bn6e_nM7.mjs";import{c as n}from"./schema-BYrYzsmO.mjs";import{a as r,c as i,d as a,i as o,o as s,r as c,s as l}from"./schema-IML1rRU_.mjs";import{a as u,c as d,d as f,f as p,i as m,l as h,n as g,o as _,r as v,s as y}from"./resolve-context-D8UXwWwr.mjs";import{r as b,t as x}from"./enumerate-CYHJKQz6.mjs";import{readSiteRecord as S}from"@murumets-ee/core";const C=4e4,w=`sitemap.xml`;async function*T(e,t,n={}){let r=I(e.maxUrlsPerFile,C),i=I(n.batchSize,200),a=N(e.baseUrl),o=[];for(let s of e.locales){let c=[],l=null,u=0,d=()=>{if(c.length===0)return null;let e=F(s,u),t=(l??new Date).toISOString(),n={kind:`urlset`,filename:e,xml:O(c)};return o.push({filename:e,lastmod:t}),c=[],l=null,u+=1,n},f=x({locale:s},t.enumerate,n.enumerate);for await(let n of L(f,i)){let i=await D(n,s,t);if(i.length===0)continue;let o=await E(i,s,e,t);for(let t of i){let n=o.get(t.id)??[{locale:s,path:t.path}];if(c.push(k(t,s,n,e,a)),(l===null||t.updatedAt>l)&&(l=t.updatedAt),c.length>=r){let e=d();e&&(yield e)}}}let p=d();p&&(yield p)}o.length>1&&(yield{kind:`index`,filename:w,xml:A(o,a)})}async function E(e,t,n,r){let i=new Map;for(let n of e)i.set(n.id,[{locale:t,path:n.path}]);let a=n.locales.filter(e=>e!==t);if(a.length===0)return i;let o=new Map;for(let t of e){let e=o.get(t.entityType);e?e.push(t.id):o.set(t.entityType,[t.id])}for(let e of a)for(let[t,n]of o){let a=await b(t,n,e,r.canonicalBatch);if(a.size===0)continue;let o=[...a.keys()],s=await r.enumerate.filterPublished(t,o,e),c=r.filterNoIndex===void 0?void 0:await r.filterNoIndex(t,o,e);for(let[t,n]of a){if(!s.has(t)||c?.has(t))continue;let r=i.get(t);r&&r.push({locale:e,path:n})}}return i}async function D(e,t,r){let i=r.filterNoIndex;if(i===void 0)return[...e];let a=new Map;for(let t of e){let e=a.get(t.entityType);e?e.push(t.id):a.set(t.entityType,[t.id])}let o=new Map;return await n([...a.entries()],5,async([e,n])=>{o.set(e,await i(e,n,t))}),e.filter(e=>!o.get(e.entityType)?.has(e.id))}function O(e){return`<?xml version="1.0" encoding="UTF-8"?>
1
+ import{i as e,t}from"./locale-registry-Bn6e_nM7.mjs";import{c as n}from"./schema-BYrYzsmO.mjs";import{a as r,c as i,d as a,i as o,o as s,r as c,s as l}from"./schema-LlK6EkOJ.mjs";import{a as u,c as d,d as f,f as p,i as m,l as h,n as g,o as _,r as v,s as y}from"./resolve-context-DklN53BH.mjs";import{r as b,t as x}from"./enumerate-CYHJKQz6.mjs";import{readSiteRecord as S}from"@murumets-ee/core";const C=4e4,w=`sitemap.xml`;async function*T(e,t,n={}){let r=I(e.maxUrlsPerFile,C),i=I(n.batchSize,200),a=N(e.baseUrl),o=[];for(let s of e.locales){let c=[],l=null,u=0,d=()=>{if(c.length===0)return null;let e=F(s,u),t=(l??new Date).toISOString(),n={kind:`urlset`,filename:e,xml:O(c)};return o.push({filename:e,lastmod:t}),c=[],l=null,u+=1,n},f=x({locale:s},t.enumerate,n.enumerate);for await(let n of L(f,i)){let i=await D(n,s,t);if(i.length===0)continue;let o=await E(i,s,e,t);for(let t of i){let n=o.get(t.id)??[{locale:s,path:t.path}];if(c.push(k(t,s,n,e,a)),(l===null||t.updatedAt>l)&&(l=t.updatedAt),c.length>=r){let e=d();e&&(yield e)}}}let p=d();p&&(yield p)}o.length>1&&(yield{kind:`index`,filename:w,xml:A(o,a)})}async function E(e,t,n,r){let i=new Map;for(let n of e)i.set(n.id,[{locale:t,path:n.path}]);let a=n.locales.filter(e=>e!==t);if(a.length===0)return i;let o=new Map;for(let t of e){let e=o.get(t.entityType);e?e.push(t.id):o.set(t.entityType,[t.id])}for(let e of a)for(let[t,n]of o){let a=await b(t,n,e,r.canonicalBatch);if(a.size===0)continue;let o=[...a.keys()],s=await r.enumerate.filterPublished(t,o,e),c=r.filterNoIndex===void 0?void 0:await r.filterNoIndex(t,o,e);for(let[t,n]of a){if(!s.has(t)||c?.has(t))continue;let r=i.get(t);r&&r.push({locale:e,path:n})}}return i}async function D(e,t,r){let i=r.filterNoIndex;if(i===void 0)return[...e];let a=new Map;for(let t of e){let e=a.get(t.entityType);e?e.push(t.id):a.set(t.entityType,[t.id])}let o=new Map;return await n([...a.entries()],5,async([e,n])=>{o.set(e,await i(e,n,t))}),e.filter(e=>!o.get(e.entityType)?.has(e.id))}function O(e){return`<?xml version="1.0" encoding="UTF-8"?>
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${e.join(`
3
3
  `)}\n</urlset>\n`}function k(e,t,n,r,i){let a=M(i,r.prefix,t,e.path),o=n.map(e=>` ${j(e.locale,M(i,r.prefix,e.locale,e.path))}`),s=n.find(e=>e.locale===r.defaultLocale);return s&&o.push(` ${j(`x-default`,M(i,r.prefix,s.locale,s.path))}`),` <url>
4
4
  <loc>${P(a)}</loc>\n <lastmod>${e.updatedAt.toISOString()}</lastmod>\n${o.join(`
@@ -1,2 +1,2 @@
1
- import{t as e}from"./content-config-store-8omQF5p5.mjs";import{n as t,s as n}from"./schema-BYrYzsmO.mjs";import{d as r,n as i,s as a}from"./schema-IML1rRU_.mjs";import{matchReservedPrefix as o}from"@murumets-ee/entity";import{combineAdminRoutes as s,defineAdminRoute as c,getContext as l,requireRequestSite as u,safeAudit as d}from"@murumets-ee/core";import{and as f,eq as p}from"drizzle-orm";import{z as m}from"zod";import{entityRefs as h}from"@murumets-ee/entity/refs";const g=`path:resolve`,ee=[`admin`,`editor`];function _(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}const v={kind:`none`};function y(e){return e===null||e.length===0?{error:`query param 'path' is required`}:e.length>2048?{error:`query param 'path' is too long`}:e.startsWith(`/`)?e.startsWith(`//`)?{error:`query param 'path' must not be protocol-relative`}:e.includes(`?`)||e.includes(`#`)?{error:`query param 'path' must not include a query or fragment`}:{path:e}:{error:`query param 'path' must start with '/'`}}function b(t,n){let r=e(),i=t??n??r.defaultLocale;return(l()?.availableLocales??r.locales.map(e=>e.code)).includes(i)?{locale:i}:{error:`query param 'locale' is not a content locale`}}const x=async(e,t)=>{let n=new URL(e.url).searchParams,r=y(n.get(`path`));if(`error`in r)return _({error:r.error},400);let i=b(n.get(`locale`),t.locale??t.defaultLocale);if(`error`in i)return _({error:i.error},400);let{locale:o}=i,{buildResolveDeps:s}=await import(`./resolve-context-D8UXwWwr.mjs`).then(e=>e.u),c=s(t.app),l=(e,n)=>t.checkPermission(e,`view`)?{kind:`document`,entityType:e,id:n}:v,u=await a(r.path,o,c);if(u.kind===`document`)return _(l(u.entityType,u.id));if(u.kind===`none`)return _(v);if(!(u.to.startsWith(`/`)&&!u.to.startsWith(`//`)))return _({kind:`redirect`,to:u.to});let d=await a(u.to,o,c);return _(d.kind===`document`?l(d.entityType,d.id):v)};function S(){return s([c({prefix:`paths`,path:`resolve`,method:`GET`,permission:g,defaultRoles:ee,description:`Resolve a site URL path to the CMS document that serves it (editor canvas link navigation).`,handler:x})])}var C=class extends Error{fromPath;prefix;constructor(e,t){super(`Redirect source "${e}" falls under the reserved prefix "${t}" and cannot be used — that path range is owned by the framework.`),this.name=`RedirectReservedPrefixError`,this.fromPath=e,this.prefix=t}},w=class extends Error{fromPath;locale;conflictingLocale;constructor(e,t,n){super(`Redirect source "${e}" already serves content in locale "${n}" (it is a live canonical URL). Choose a different source path, or unpublish/rename the content first.`),this.name=`RedirectServesContentError`,this.fromPath=e,this.locale=t,this.conflictingLocale=n}},T=class extends Error{entity;constructor(e){super(`Redirect target references entity type "${e}", which is not a registered entity. Check the entity name.`),this.name=`RedirectUnknownEntityError`,this.entity=e}},E=class extends Error{entity;id;constructor(e,t){super(`Redirect target ${e} "${t}" does not exist. Redirect to a document that exists, or use a URL/path target instead.`),this.name=`RedirectTargetNotFoundError`,this.entity=e,this.id=t}},D=class extends Error{fromPath;locale;constructor(e,t){super(`Redirect source "${e}"${t===null?``:` (locale "${t}")`} resolves back to itself — a redirect cannot point at its own source path. Choose a different destination.`),this.name=`RedirectSelfReferenceError`,this.fromPath=e,this.locale=t}};const O=`toolkit_redirects`;async function k(e,t,n){n.kind===`entity`&&await e.insert(h).values({sourceEntity:O,sourceId:t,sourceField:`target`,targetEntity:n.entity,targetId:n.id,relationKind:`link`}).onConflictDoNothing()}async function A(e,t){await e.delete(h).where(f(p(h.sourceEntity,O),p(h.sourceId,t)))}const j=m.discriminatedUnion(`kind`,[m.object({kind:m.literal(`entity`),entity:m.string().min(1),id:m.string().uuid()}),m.object({kind:m.literal(`url`),url:m.string().max(2048).url()}),m.object({kind:m.literal(`path`),path:m.string().min(1).max(2048).startsWith(`/`)})]);function M(e){return r(e)}function N(e){let t=M(e.fromPath),n=o(t,e.reservedPathPrefixes);if(n!==void 0)throw new C(t,n)}async function te(e){let t=M(e.fromPath);if(e.locale!==null){let n=await e.pathsClient.findOne({path:t,isCanonical:!0,locale:e.locale});if(n)throw new w(t,e.locale,n.locale);return}let n=await e.pathsClient.findOne({path:t,isCanonical:!0,locale:{in:[...e.configuredLocales]}});if(n)throw new w(t,null,n.locale)}async function ne(e){if(e.target.kind!==`path`)return e.target;let t=M(e.target.path),n=e.excludeId===void 0?{}:{id:{ne:e.excludeId}};if(e.locale!==null){let r=await e.redirectsClient.findOne({fromPath:t,locale:e.locale,...n});if(r)return r.target}let r=await e.redirectsClient.findOne({fromPath:t,locale:null,...n});return r?r.target:{kind:`path`,path:t}}async function P(e){if(e.target.kind!==`entity`)return;let{entity:t,id:n}=e.target,r=e.resolveClient(t);if(r===void 0)throw new T(t);if(await r.findById(n)==null)throw new E(t,n)}function F(){return u(`toolkit_redirects`)}const I=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,L=/^[a-zA-Z0-9:_.-]{1,100}$/,R=`redirect:manage`,z=[`admin`];function B(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function V(e,t){return B({error:e},t)}async function H(e,t){let n;try{n=await e.json()}catch{return{response:V(`Invalid JSON body`,400)}}let r=t.safeParse(n);if(!r.success){let e=r.error.issues[0];return{response:V(e?`${e.path.join(`.`)||`body`}: ${e.message}`:`Invalid body`,400)}}return{data:r.data}}function U(e,t){if(e===null||!/^\d+$/.test(e))return t;let n=Number(e);return Number.isSafeInteger(n)?n:t}function W(e){return e.name===void 0?{userId:e.id}:{userId:e.id,userName:e.name}}async function G(){let{getApp:e}=await import(`@murumets-ee/core`);return e()}async function K(){return(await G()).db.readWrite}async function q(){let e=await G();return t=>{let n=e.entities.get(t);return n?e.getClient(n):void 0}}function J(e){return e}const Y=m.union([m.literal(301),m.literal(302)]),X=m.string().min(1).max(2048).startsWith(`/`),re=m.object({locale:m.string().min(1).nullable(),fromPath:X,target:j,status:Y.default(301)}),ie=m.object({locale:m.string().min(1).nullable().optional(),fromPath:X.optional(),target:j.optional(),status:Y.optional()});function Z(t){return t===null||e().locales.map(e=>e.code).includes(t)?null:V(`Unknown locale "${t}" — not one of the configured content locales.`,400)}function Q(e){if(e instanceof C)return B({error:e.message,fromPath:e.fromPath,prefix:e.prefix},409);if(e instanceof w)return B({error:e.message,fromPath:e.fromPath,locale:e.locale,conflictingLocale:e.conflictingLocale},409);if(e instanceof D)return B({error:e.message,fromPath:e.fromPath,locale:e.locale},409);if(e instanceof T)return B({error:e.message,entity:e.entity},400);if(e instanceof E)return B({error:e.message,entity:e.entity,id:e.id},400);if(n(e))return B({error:`A redirect already exists for this locale + source path.`},409);throw e}async function $(n){let r=e(),a=r.reservedPathPrefixes??[],o=r.locales.map(e=>e.code),s=M(n.fromPath);N({fromPath:s,reservedPathPrefixes:a}),await P({target:n.target,resolveClient:n.resolveClient});let c=await K(),l=await F();return c.transaction(async e=>{let r=J(e),a=i.makeClient(r,l),c=t.makeClient(r,l);await te({fromPath:s,locale:n.locale,configuredLocales:o,pathsClient:c});let u=await ne({target:n.target,locale:n.locale,redirectsClient:a,...n.id===null?{}:{excludeId:n.id}});if(u.kind===`path`&&M(u.path)===s)throw new D(s,n.locale);if(n.id===null){let e=await a.insert({locale:n.locale,fromPath:s,target:u,status:n.status,createdBy:n.userId});return await k(r,e.id,u),e}let d=await a.update({id:n.id},{locale:n.locale,fromPath:s,target:u,status:n.status,updatedAt:new Date});return d?(await A(r,n.id),await k(r,n.id,u),d):null})}const ae=async e=>{let t=new URL(e.url),n=t.searchParams.get(`locale`),r=Math.min(Math.max(U(t.searchParams.get(`limit`),50),1),200),a=Math.min(U(t.searchParams.get(`offset`),0),1e5),o;if(n!==null&&n!==``){let e=Z(n);if(e)return e;o=n}let s=i.makeClient(await K(),await F());return B({items:o===void 0?await s.findMany({orderBy:[{column:`createdAt`,dir:`desc`}],limit:r,offset:a}):await s.findMany({where:{locale:o},orderBy:[{column:`createdAt`,dir:`desc`}],limit:r,offset:a}),total:o===void 0?await s.count():await s.count({locale:o})})},oe=async(e,t)=>{let n=await H(e,re);if(`response`in n)return n.response;let r=n.data,i=Z(r.locale);if(i)return i;try{let e=await q(),n=await $({id:null,locale:r.locale,fromPath:r.fromPath,target:r.target,status:r.status,userId:t.user.id,resolveClient:e});return n?(d(t,{action:`redirect.created`,entityType:`redirect`,entityId:n.id,...W(t.user),metadata:{locale:n.locale,fromPath:n.fromPath,targetKind:n.target.kind}}),B({item:n},201)):V(`Failed to create redirect`,500)}catch(e){return Q(e)}},se=async(e,t)=>{let n=t.segments[0];if(!n||!I.test(n))return V(`Invalid id`,400);let r=await H(e,ie);if(`response`in r)return r.response;let a=r.data,o=await i.makeClient(await K(),await F()).findOne({id:n});if(!o)return V(`Not found`,404);let s=a.locale===void 0?o.locale:a.locale,c=a.fromPath===void 0?o.fromPath:a.fromPath,l=a.target===void 0?o.target:a.target,u=a.status===void 0?o.status:a.status,f=Z(s);if(f)return f;try{let e=await q(),r=await $({id:n,locale:s,fromPath:c,target:l,status:u,userId:t.user.id,resolveClient:e});return r?(d(t,{action:`redirect.updated`,entityType:`redirect`,entityId:r.id,...W(t.user),metadata:{locale:r.locale,fromPath:r.fromPath,targetKind:r.target.kind}}),B({item:r})):V(`Not found`,404)}catch(e){return Q(e)}},ce=async(e,t)=>{let n=t.segments[0];if(!n||!I.test(n))return V(`Invalid id`,400);let r=await K(),a=await F(),o=await r.transaction(async e=>{let t=J(e),r=await i.makeClient(t,a).delete({id:n});return r&&await A(t,n),r});return o?(d(t,{action:`redirect.deleted`,entityType:`redirect`,entityId:n,...W(t.user),metadata:{locale:o.locale,fromPath:o.fromPath}}),B({ok:!0})):V(`Not found`,404)},le=async e=>{let n=new URL(e.url),r=n.searchParams.get(`entityType`),i=n.searchParams.get(`entityId`);if(!r||!L.test(r))return V(`entityType is required`,400);if(!i||!I.test(i))return V(`entityId is required and must be a UUID`,400);let a=n.searchParams.get(`locale`),o;if(a!==null&&a!==``){let e=Z(a);if(e)return e;o=a}return B({items:await t.makeClient(await K(),await F()).findMany({where:{entityType:r,entityId:i,isCanonical:!1,...o!==void 0&&{locale:o}},orderBy:[{column:`updatedAt`,dir:`desc`}],limit:100})})};function ue(){return s([c({prefix:`redirects`,path:``,method:`GET`,permission:R,defaultRoles:z,description:`List manual URL redirects (paginated, optional locale filter).`,handler:ae}),c({prefix:`redirects`,path:`history`,method:`GET`,permission:R,defaultRoles:z,description:`Read-only automatic-redirect (rename) history for one document.`,handler:le}),c({prefix:`redirects`,path:``,method:`POST`,permission:R,defaultRoles:z,description:`Create a manual URL redirect.`,handler:oe}),c({prefix:`redirects`,path:``,method:`PATCH`,matchAnyPath:!0,permission:R,defaultRoles:z,description:`Update a manual URL redirect by id.`,handler:se}),c({prefix:`redirects`,path:``,method:`DELETE`,matchAnyPath:!0,permission:R,defaultRoles:z,description:`Delete a manual URL redirect by id.`,handler:ce})])}export{g as n,S as r,ue as t};
2
- //# sourceMappingURL=routes-DlvqU8bO.mjs.map
1
+ import{t as e}from"./content-config-store-8omQF5p5.mjs";import{n as t,s as n}from"./schema-BYrYzsmO.mjs";import{d as r,n as i,s as a}from"./schema-LlK6EkOJ.mjs";import{matchReservedPrefix as o}from"@murumets-ee/entity";import{combineAdminRoutes as s,defineAdminRoute as c,getContext as l,requireRequestSite as u,safeAudit as d}from"@murumets-ee/core";import{and as f,eq as p}from"drizzle-orm";import{z as m}from"zod";import{entityRefs as h}from"@murumets-ee/entity/refs";const g=`path:resolve`,ee=[`admin`,`editor`];function _(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}const v={kind:`none`};function y(e){return e===null||e.length===0?{error:`query param 'path' is required`}:e.length>2048?{error:`query param 'path' is too long`}:e.startsWith(`/`)?e.startsWith(`//`)?{error:`query param 'path' must not be protocol-relative`}:e.includes(`?`)||e.includes(`#`)?{error:`query param 'path' must not include a query or fragment`}:{path:e}:{error:`query param 'path' must start with '/'`}}function b(t,n){let r=e(),i=t??n??r.defaultLocale;return(l()?.availableLocales??r.locales.map(e=>e.code)).includes(i)?{locale:i}:{error:`query param 'locale' is not a content locale`}}const x=async(e,t)=>{let n=new URL(e.url).searchParams,r=y(n.get(`path`));if(`error`in r)return _({error:r.error},400);let i=b(n.get(`locale`),t.locale??t.defaultLocale);if(`error`in i)return _({error:i.error},400);let{locale:o}=i,{buildResolveDeps:s}=await import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),c=s(t.app),l=(e,n)=>t.checkPermission(e,`view`)?{kind:`document`,entityType:e,id:n}:v,u=await a(r.path,o,c);if(u.kind===`document`)return _(l(u.entityType,u.id));if(u.kind===`none`)return _(v);if(!(u.to.startsWith(`/`)&&!u.to.startsWith(`//`)))return _({kind:`redirect`,to:u.to});let d=await a(u.to,o,c);return _(d.kind===`document`?l(d.entityType,d.id):v)};function S(){return s([c({prefix:`paths`,path:`resolve`,method:`GET`,permission:g,defaultRoles:ee,description:`Resolve a site URL path to the CMS document that serves it (editor canvas link navigation).`,handler:x})])}var C=class extends Error{fromPath;prefix;constructor(e,t){super(`Redirect source "${e}" falls under the reserved prefix "${t}" and cannot be used — that path range is owned by the framework.`),this.name=`RedirectReservedPrefixError`,this.fromPath=e,this.prefix=t}},w=class extends Error{fromPath;locale;conflictingLocale;constructor(e,t,n){super(`Redirect source "${e}" already serves content in locale "${n}" (it is a live canonical URL). Choose a different source path, or unpublish/rename the content first.`),this.name=`RedirectServesContentError`,this.fromPath=e,this.locale=t,this.conflictingLocale=n}},T=class extends Error{entity;constructor(e){super(`Redirect target references entity type "${e}", which is not a registered entity. Check the entity name.`),this.name=`RedirectUnknownEntityError`,this.entity=e}},E=class extends Error{entity;id;constructor(e,t){super(`Redirect target ${e} "${t}" does not exist. Redirect to a document that exists, or use a URL/path target instead.`),this.name=`RedirectTargetNotFoundError`,this.entity=e,this.id=t}},D=class extends Error{fromPath;locale;constructor(e,t){super(`Redirect source "${e}"${t===null?``:` (locale "${t}")`} resolves back to itself — a redirect cannot point at its own source path. Choose a different destination.`),this.name=`RedirectSelfReferenceError`,this.fromPath=e,this.locale=t}};const O=`toolkit_redirects`;async function k(e,t,n){n.kind===`entity`&&await e.insert(h).values({sourceEntity:O,sourceId:t,sourceField:`target`,targetEntity:n.entity,targetId:n.id,relationKind:`link`}).onConflictDoNothing()}async function A(e,t){await e.delete(h).where(f(p(h.sourceEntity,O),p(h.sourceId,t)))}const j=m.discriminatedUnion(`kind`,[m.object({kind:m.literal(`entity`),entity:m.string().min(1),id:m.string().uuid()}),m.object({kind:m.literal(`url`),url:m.string().max(2048).url()}),m.object({kind:m.literal(`path`),path:m.string().min(1).max(2048).startsWith(`/`)})]);function M(e){return r(e)}function N(e){let t=M(e.fromPath),n=o(t,e.reservedPathPrefixes);if(n!==void 0)throw new C(t,n)}async function te(e){let t=M(e.fromPath);if(e.locale!==null){let n=await e.pathsClient.findOne({path:t,isCanonical:!0,locale:e.locale});if(n)throw new w(t,e.locale,n.locale);return}let n=await e.pathsClient.findOne({path:t,isCanonical:!0,locale:{in:[...e.configuredLocales]}});if(n)throw new w(t,null,n.locale)}async function ne(e){if(e.target.kind!==`path`)return e.target;let t=M(e.target.path),n=e.excludeId===void 0?{}:{id:{ne:e.excludeId}};if(e.locale!==null){let r=await e.redirectsClient.findOne({fromPath:t,locale:e.locale,...n});if(r)return r.target}let r=await e.redirectsClient.findOne({fromPath:t,locale:null,...n});return r?r.target:{kind:`path`,path:t}}async function P(e){if(e.target.kind!==`entity`)return;let{entity:t,id:n}=e.target,r=e.resolveClient(t);if(r===void 0)throw new T(t);if(await r.findById(n)==null)throw new E(t,n)}function F(){return u(`toolkit_redirects`)}const I=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,L=/^[a-zA-Z0-9:_.-]{1,100}$/,R=`redirect:manage`,z=[`admin`];function B(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function V(e,t){return B({error:e},t)}async function H(e,t){let n;try{n=await e.json()}catch{return{response:V(`Invalid JSON body`,400)}}let r=t.safeParse(n);if(!r.success){let e=r.error.issues[0];return{response:V(e?`${e.path.join(`.`)||`body`}: ${e.message}`:`Invalid body`,400)}}return{data:r.data}}function U(e,t){if(e===null||!/^\d+$/.test(e))return t;let n=Number(e);return Number.isSafeInteger(n)?n:t}function W(e){return e.name===void 0?{userId:e.id}:{userId:e.id,userName:e.name}}async function G(){let{getApp:e}=await import(`@murumets-ee/core`);return e()}async function K(){return(await G()).db.readWrite}async function q(){let e=await G();return t=>{let n=e.entities.get(t);return n?e.getClient(n):void 0}}function J(e){return e}const Y=m.union([m.literal(301),m.literal(302)]),X=m.string().min(1).max(2048).startsWith(`/`),re=m.object({locale:m.string().min(1).nullable(),fromPath:X,target:j,status:Y.default(301)}),ie=m.object({locale:m.string().min(1).nullable().optional(),fromPath:X.optional(),target:j.optional(),status:Y.optional()});function Z(t){return t===null||e().locales.map(e=>e.code).includes(t)?null:V(`Unknown locale "${t}" — not one of the configured content locales.`,400)}function Q(e){if(e instanceof C)return B({error:e.message,fromPath:e.fromPath,prefix:e.prefix},409);if(e instanceof w)return B({error:e.message,fromPath:e.fromPath,locale:e.locale,conflictingLocale:e.conflictingLocale},409);if(e instanceof D)return B({error:e.message,fromPath:e.fromPath,locale:e.locale},409);if(e instanceof T)return B({error:e.message,entity:e.entity},400);if(e instanceof E)return B({error:e.message,entity:e.entity,id:e.id},400);if(n(e))return B({error:`A redirect already exists for this locale + source path.`},409);throw e}async function $(n){let r=e(),a=r.reservedPathPrefixes??[],o=r.locales.map(e=>e.code),s=M(n.fromPath);N({fromPath:s,reservedPathPrefixes:a}),await P({target:n.target,resolveClient:n.resolveClient});let c=await K(),l=await F();return c.transaction(async e=>{let r=J(e),a=i.makeClient(r,l),c=t.makeClient(r,l);await te({fromPath:s,locale:n.locale,configuredLocales:o,pathsClient:c});let u=await ne({target:n.target,locale:n.locale,redirectsClient:a,...n.id===null?{}:{excludeId:n.id}});if(u.kind===`path`&&M(u.path)===s)throw new D(s,n.locale);if(n.id===null){let e=await a.insert({locale:n.locale,fromPath:s,target:u,status:n.status,createdBy:n.userId});return await k(r,e.id,u),e}let d=await a.update({id:n.id},{locale:n.locale,fromPath:s,target:u,status:n.status,updatedAt:new Date});return d?(await A(r,n.id),await k(r,n.id,u),d):null})}const ae=async e=>{let t=new URL(e.url),n=t.searchParams.get(`locale`),r=Math.min(Math.max(U(t.searchParams.get(`limit`),50),1),200),a=Math.min(U(t.searchParams.get(`offset`),0),1e5),o;if(n!==null&&n!==``){let e=Z(n);if(e)return e;o=n}let s=i.makeClient(await K(),await F());return B({items:o===void 0?await s.findMany({orderBy:[{column:`createdAt`,dir:`desc`}],limit:r,offset:a}):await s.findMany({where:{locale:o},orderBy:[{column:`createdAt`,dir:`desc`}],limit:r,offset:a}),total:o===void 0?await s.count():await s.count({locale:o})})},oe=async(e,t)=>{let n=await H(e,re);if(`response`in n)return n.response;let r=n.data,i=Z(r.locale);if(i)return i;try{let e=await q(),n=await $({id:null,locale:r.locale,fromPath:r.fromPath,target:r.target,status:r.status,userId:t.user.id,resolveClient:e});return n?(d(t,{action:`redirect.created`,entityType:`redirect`,entityId:n.id,...W(t.user),metadata:{locale:n.locale,fromPath:n.fromPath,targetKind:n.target.kind}}),B({item:n},201)):V(`Failed to create redirect`,500)}catch(e){return Q(e)}},se=async(e,t)=>{let n=t.segments[0];if(!n||!I.test(n))return V(`Invalid id`,400);let r=await H(e,ie);if(`response`in r)return r.response;let a=r.data,o=await i.makeClient(await K(),await F()).findOne({id:n});if(!o)return V(`Not found`,404);let s=a.locale===void 0?o.locale:a.locale,c=a.fromPath===void 0?o.fromPath:a.fromPath,l=a.target===void 0?o.target:a.target,u=a.status===void 0?o.status:a.status,f=Z(s);if(f)return f;try{let e=await q(),r=await $({id:n,locale:s,fromPath:c,target:l,status:u,userId:t.user.id,resolveClient:e});return r?(d(t,{action:`redirect.updated`,entityType:`redirect`,entityId:r.id,...W(t.user),metadata:{locale:r.locale,fromPath:r.fromPath,targetKind:r.target.kind}}),B({item:r})):V(`Not found`,404)}catch(e){return Q(e)}},ce=async(e,t)=>{let n=t.segments[0];if(!n||!I.test(n))return V(`Invalid id`,400);let r=await K(),a=await F(),o=await r.transaction(async e=>{let t=J(e),r=await i.makeClient(t,a).delete({id:n});return r&&await A(t,n),r});return o?(d(t,{action:`redirect.deleted`,entityType:`redirect`,entityId:n,...W(t.user),metadata:{locale:o.locale,fromPath:o.fromPath}}),B({ok:!0})):V(`Not found`,404)},le=async e=>{let n=new URL(e.url),r=n.searchParams.get(`entityType`),i=n.searchParams.get(`entityId`);if(!r||!L.test(r))return V(`entityType is required`,400);if(!i||!I.test(i))return V(`entityId is required and must be a UUID`,400);let a=n.searchParams.get(`locale`),o;if(a!==null&&a!==``){let e=Z(a);if(e)return e;o=a}return B({items:await t.makeClient(await K(),await F()).findMany({where:{entityType:r,entityId:i,isCanonical:!1,...o!==void 0&&{locale:o}},orderBy:[{column:`updatedAt`,dir:`desc`}],limit:100})})};function ue(){return s([c({prefix:`redirects`,path:``,method:`GET`,permission:R,defaultRoles:z,description:`List manual URL redirects (paginated, optional locale filter).`,handler:ae}),c({prefix:`redirects`,path:`history`,method:`GET`,permission:R,defaultRoles:z,description:`Read-only automatic-redirect (rename) history for one document.`,handler:le}),c({prefix:`redirects`,path:``,method:`POST`,permission:R,defaultRoles:z,description:`Create a manual URL redirect.`,handler:oe}),c({prefix:`redirects`,path:``,method:`PATCH`,matchAnyPath:!0,permission:R,defaultRoles:z,description:`Update a manual URL redirect by id.`,handler:se}),c({prefix:`redirects`,path:``,method:`DELETE`,matchAnyPath:!0,permission:R,defaultRoles:z,description:`Delete a manual URL redirect by id.`,handler:ce})])}export{g as n,S as r,ue as t};
2
+ //# sourceMappingURL=routes-B6TJ5qiR.mjs.map