@aglyn/tenant-runtime 1.0.0-beta.143

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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +26 -0
  3. package/package.json +39 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/apply-publish-schedule.d.ts +34 -0
  8. package/src/lib/apply-publish-schedule.js +285 -0
  9. package/src/lib/apply-publish-schedule.js.map +1 -0
  10. package/src/lib/assign-contact-owner.d.ts +162 -0
  11. package/src/lib/assign-contact-owner.js +322 -0
  12. package/src/lib/assign-contact-owner.js.map +1 -0
  13. package/src/lib/associate-company-by-domain.d.ts +70 -0
  14. package/src/lib/associate-company-by-domain.js +150 -0
  15. package/src/lib/associate-company-by-domain.js.map +1 -0
  16. package/src/lib/author-page-nodes.d.ts +36 -0
  17. package/src/lib/author-page-nodes.js +296 -0
  18. package/src/lib/author-page-nodes.js.map +1 -0
  19. package/src/lib/built-in-page-layout.d.ts +48 -0
  20. package/src/lib/built-in-page-layout.js +107 -0
  21. package/src/lib/built-in-page-layout.js.map +1 -0
  22. package/src/lib/capture-host-contact.d.ts +95 -0
  23. package/src/lib/capture-host-contact.js +137 -0
  24. package/src/lib/capture-host-contact.js.map +1 -0
  25. package/src/lib/collection-fallback-nodes.d.ts +90 -0
  26. package/src/lib/collection-fallback-nodes.js +602 -0
  27. package/src/lib/collection-fallback-nodes.js.map +1 -0
  28. package/src/lib/compose-author-page.d.ts +109 -0
  29. package/src/lib/compose-author-page.js +225 -0
  30. package/src/lib/compose-author-page.js.map +1 -0
  31. package/src/lib/compose-collection-page.d.ts +94 -0
  32. package/src/lib/compose-collection-page.js +256 -0
  33. package/src/lib/compose-collection-page.js.map +1 -0
  34. package/src/lib/compose-screen-nodes.d.ts +156 -0
  35. package/src/lib/compose-screen-nodes.js +547 -0
  36. package/src/lib/compose-screen-nodes.js.map +1 -0
  37. package/src/lib/compose-search-page.d.ts +39 -0
  38. package/src/lib/compose-search-page.js +57 -0
  39. package/src/lib/compose-search-page.js.map +1 -0
  40. package/src/lib/convert-host-lead.d.ts +189 -0
  41. package/src/lib/convert-host-lead.js +443 -0
  42. package/src/lib/convert-host-lead.js.map +1 -0
  43. package/src/lib/defer-lazy-panels.d.ts +58 -0
  44. package/src/lib/defer-lazy-panels.js +171 -0
  45. package/src/lib/defer-lazy-panels.js.map +1 -0
  46. package/src/lib/element-animation-assets.d.ts +62 -0
  47. package/src/lib/element-animation-assets.js +290 -0
  48. package/src/lib/element-animation-assets.js.map +1 -0
  49. package/src/lib/emit-host-event.d.ts +29 -0
  50. package/src/lib/emit-host-event.js +30 -0
  51. package/src/lib/emit-host-event.js.map +1 -0
  52. package/src/lib/enrich-gated-page.d.ts +67 -0
  53. package/src/lib/enrich-gated-page.js +134 -0
  54. package/src/lib/enrich-gated-page.js.map +1 -0
  55. package/src/lib/entry-link-routes.d.ts +61 -0
  56. package/src/lib/entry-link-routes.js +169 -0
  57. package/src/lib/entry-link-routes.js.map +1 -0
  58. package/src/lib/get-author-content.d.ts +119 -0
  59. package/src/lib/get-author-content.js +288 -0
  60. package/src/lib/get-author-content.js.map +1 -0
  61. package/src/lib/get-collection-content.d.ts +290 -0
  62. package/src/lib/get-collection-content.js +565 -0
  63. package/src/lib/get-collection-content.js.map +1 -0
  64. package/src/lib/get-components.d.ts +24 -0
  65. package/src/lib/get-components.js +102 -0
  66. package/src/lib/get-components.js.map +1 -0
  67. package/src/lib/get-datasets.d.ts +45 -0
  68. package/src/lib/get-datasets.js +169 -0
  69. package/src/lib/get-datasets.js.map +1 -0
  70. package/src/lib/get-forms.d.ts +24 -0
  71. package/src/lib/get-forms.js +116 -0
  72. package/src/lib/get-forms.js.map +1 -0
  73. package/src/lib/get-layout-version.d.ts +26 -0
  74. package/src/lib/get-layout-version.js +99 -0
  75. package/src/lib/get-layout-version.js.map +1 -0
  76. package/src/lib/get-media-asset-facts.d.ts +74 -0
  77. package/src/lib/get-media-asset-facts.js +116 -0
  78. package/src/lib/get-media-asset-facts.js.map +1 -0
  79. package/src/lib/get-plugin-installs.d.ts +29 -0
  80. package/src/lib/get-plugin-installs.js +94 -0
  81. package/src/lib/get-plugin-installs.js.map +1 -0
  82. package/src/lib/get-screen-version.d.ts +27 -0
  83. package/src/lib/get-screen-version.js +66 -0
  84. package/src/lib/get-screen-version.js.map +1 -0
  85. package/src/lib/get-screen.d.ts +39 -0
  86. package/src/lib/get-screen.js +126 -0
  87. package/src/lib/get-screen.js.map +1 -0
  88. package/src/lib/get-variables.d.ts +41 -0
  89. package/src/lib/get-variables.js +128 -0
  90. package/src/lib/get-variables.js.map +1 -0
  91. package/src/lib/get-video-asset-facts.d.ts +22 -0
  92. package/src/lib/get-video-asset-facts.js +22 -0
  93. package/src/lib/get-video-asset-facts.js.map +1 -0
  94. package/src/lib/host-event-listeners.d.ts +83 -0
  95. package/src/lib/host-event-listeners.js +91 -0
  96. package/src/lib/host-event-listeners.js.map +1 -0
  97. package/src/lib/org-permissions.d.ts +63 -0
  98. package/src/lib/org-permissions.js +187 -0
  99. package/src/lib/org-permissions.js.map +1 -0
  100. package/src/lib/repeat-record-pages.d.ts +48 -0
  101. package/src/lib/repeat-record-pages.js +64 -0
  102. package/src/lib/repeat-record-pages.js.map +1 -0
  103. package/src/lib/required-site-plugins.d.ts +114 -0
  104. package/src/lib/required-site-plugins.js +121 -0
  105. package/src/lib/required-site-plugins.js.map +1 -0
  106. package/src/lib/resolve-dataset.d.ts +40 -0
  107. package/src/lib/resolve-dataset.js +64 -0
  108. package/src/lib/resolve-dataset.js.map +1 -0
  109. package/src/lib/search-results-nodes.d.ts +48 -0
  110. package/src/lib/search-results-nodes.js +302 -0
  111. package/src/lib/search-results-nodes.js.map +1 -0
  112. package/src/lib/social-image-facts.d.ts +107 -0
  113. package/src/lib/social-image-facts.js +133 -0
  114. package/src/lib/social-image-facts.js.map +1 -0
  115. package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
  116. package/src/lib/stamp-form-dataset-bindings.js +59 -0
  117. package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
  118. package/src/lib/template-screens.d.ts +227 -0
  119. package/src/lib/template-screens.js +364 -0
  120. package/src/lib/template-screens.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-collection-content.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHORS_MAX_PER_HOST,\n type CollectionCategory,\n collectionCategorySlug,\n checkEntitlement,\n COLLECTION_SOURCE_MAX,\n collectionTotalPages,\n type ContentAuthorRecord,\n entryMatchesCategoryRoute,\n hostCollectionKind,\n normalizeContentAuthor,\n normalizeContentSchemaType,\n resolveCollectionCategoryBySlug,\n resolveEntryAuthor,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * ONE cached read per collection, shared by every surface that lists it\n * (AGL-1302).\n *\n * It began as the compose-time source only: a Collection entries block in a\n * shared layout re-read up to ~100 entry docs on EVERY page of the site. The\n * routed listing was left out on the argument that the page's own ISR entry\n * amortized it — which is true of ONE address and false of a collection,\n * because a collection is not one address. `/blog`, `/blog/page/2…10`, a\n * `/blog/category/{slug}` per category and `/blog/rss.xml` are all the same\n * data, each paying its own `1 + entries + authors` per window, beside a\n * cache already holding exactly that.\n *\n * The other half of that argument was real and is answered rather than\n * dropped: `flipDueEntry` is a write, and nothing else publishes a content\n * entry, so a cache that stored a collection with a schedule still pending\n * would suppress the render that publishes it. `getPublishedCollectionSource`\n * therefore declines to STORE exactly those collections — see its `store`\n * predicate — which leaves scheduled publishing on the render window it has\n * always been on, and puts everything else on this TTL.\n */\nconst COLLECTION_SOURCE_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * Resolve a public content-collection slug (AGL-954). Commerce's product\n * collections share `hosts/{hostId}/collections`, and a slug is only unique\n * within a kind — a bare `limit(1)` handed the URL to whichever doc Firestore\n * returned first, so a catalog collection could shadow a blog. Reads a small\n * window instead and takes the first content-kind match.\n */\nasync function findContentCollection(\n hostId: string,\n collectionSlug: string,\n): Promise<FirebaseFirestore.QueryDocumentSnapshot | undefined> {\n const matches = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .where('slug', '==', collectionSlug)\n .limit(5)\n .get()\n return matches.docs.find(\n (docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'content',\n )\n}\n\nexport interface CollectionEntrySummary {\n $id: string\n title: string\n slug: string\n excerpt?: string\n /**\n * The byline TEXT (AGL-686). Either the entry's own legacy free-typed\n * string or — since AGL-2486 — the name of the author record `authorId`\n * points at, resolved here so every downstream reader (the Entry Meta\n * block, `{{entry.author}}`, the RSS feed) keeps asking one field.\n */\n authorName?: string\n /** Reference into `hosts/{hostId}/authors` (AGL-2486). */\n authorId?: string\n /**\n * The resolved author RECORD (AGL-2486) — what `Article.author` is built\n * from. Null when the entry names no author, in which case the page falls\n * back to the site's publisher entity exactly as it always has.\n */\n author?: ContentAuthorRecord | null\n body?: string\n coverImage?: string\n /** `og:image:alt` for the cover (AGL-2417); travels WITH `coverImage`. */\n coverImageAlt?: string\n /**\n * The featured video (AGL-2956): a media reference or a URL, a Wistia link\n * included, in the shape `coverImage` is. See\n * `CollectionEntryRecord.coverVideo`.\n */\n coverVideo?: string\n /** Search-result title override (AGL-582); falls back to `title`. */\n seoTitle?: string\n /** Meta description override (AGL-582); falls back to `excerpt`. */\n seoDescription?: string\n /**\n * Stable reference into the collection's `categories` taxonomy\n * (AGL-582); resolved to a display name at render.\n */\n categoryId?: string\n /** Legacy free-typed bucket (AGL-582); read-only fallback. */\n category?: string\n /** Free-form labels (AGL-582). */\n tags?: string[]\n publishedAt?: { seconds: number } | null\n /**\n * Last edited, which is what `Article.dateModified` publishes (AGL-2534).\n *\n * Distinct from {@link publishedAt} on purpose: re-dating a post is not\n * editing it, so the console writes `publishedAt` alone when an author\n * backdates and this stays put. Google reads `dateModified` for freshness.\n */\n updatedAt?: { seconds: number } | null\n /**\n * The collection this entry came out of (AGL-2518), stamped only by a\n * reader that MIXES collections — the author page. Unset on every routed\n * listing, where the route already answers the question. See\n * `CollectionEntryRecord.collectionSlug`.\n */\n collectionSlug?: string\n /** The display name of {@link collectionSlug}. */\n collectionName?: string\n}\n\n/** Entry-doc fields shared by the list and single-entry mappers (AGL-582). */\nfunction mapEntryFields(\n value: FirebaseFirestore.DocumentData,\n): Pick<\n CollectionEntrySummary,\n | 'excerpt'\n | 'coverImage'\n | 'coverImageAlt'\n | 'coverVideo'\n | 'seoTitle'\n | 'seoDescription'\n | 'authorName'\n | 'authorId'\n | 'categoryId'\n | 'category'\n | 'tags'\n | 'updatedAt'\n> {\n return {\n excerpt: value['excerpt'] ?? '',\n // The byline was DECLARED on `CollectionEntrySummary` (AGL-686) and\n // mapped by nobody, so `entry.authorName` was `undefined` on every entry\n // this loader returned — which is every routed entry page and every\n // Collection entries block. The console collected the field, the rules\n // stored it, the JSON-LD builder read it and the Entry Meta block printed\n // it, and all three saw nothing, because the one hop between Firestore\n // and them dropped it (AGL-2486). Written but never read, in the\n // direction that leaves no error behind.\n authorName: value['authorName'] ?? '',\n authorId: value['authorId'] ?? '',\n coverImage: value['coverImage'] ?? '',\n coverImageAlt: value['coverImageAlt'] ?? '',\n // Here, where both read paths pick it up, so a list card and the routed\n // entry page can each bind the featured video (AGL-2956).\n coverVideo: value['coverVideo'] ?? '',\n seoTitle: value['seoTitle'] ?? '',\n seoDescription: value['seoDescription'] ?? '',\n categoryId: value['categoryId'] ?? '',\n category: value['category'] ?? '',\n tags: Array.isArray(value['tags'])\n ? value['tags'].filter((tag): tag is string => typeof tag === 'string')\n : [],\n /*\n `dateModified`'s source (AGL-2534), and it was missing for the same\n reason `authorName` was — the reason this function's own comment above\n describes.\n\n The console has written `updatedAt` on every save, `page.tsx` reads\n `entry.updatedAt.seconds` to publish `Article.dateModified`, a spec\n asserts the conversion, and two console comments explain why it must not\n track `publishedAt`. Nothing mapped it, so `entry.updatedAt` was\n `undefined` on every entry the loader has ever returned and no published\n article has ever carried a `dateModified` — the freshness signal Google\n reads. The spec passed throughout because it builds its entry object by\n hand and never crosses this boundary.\n\n Mapped HERE rather than beside `publishedAt` at the two call sites: this\n is an ordinary field with no `publishAt` fallback and nothing sorts on\n it, so one place is enough — and one place is what stops the next read\n path from forgetting it.\n */\n updatedAt: value['updatedAt']?.seconds\n ? { seconds: value['updatedAt'].seconds }\n : null,\n }\n}\n\n/**\n * The collection doc's category taxonomy (AGL-582), sanitized: only\n * `{ id, name }` pairs with non-empty strings survive, order preserved.\n *\n * `description` rides along when the author wrote one and is dropped when it\n * is blank or not a string, so the head can tell \"described\" from \"not\n * described\" by truthiness alone — an empty string reaching the metadata\n * would suppress the template screen's description and leave the listing with\n * none at all.\n */\nfunction mapCollectionCategories(value: unknown): CollectionCategory[] {\n if (!Array.isArray(value)) return []\n return value\n .filter(\n (item): item is CollectionCategory =>\n typeof item?.id === 'string' &&\n item.id.trim() !== '' &&\n typeof item?.name === 'string' &&\n item.name.trim() !== '',\n )\n .map((item) => {\n const description =\n typeof item.description === 'string' ? item.description.trim() : ''\n return {\n id: item.id,\n name: item.name,\n ...(description ? { description } : {}),\n }\n })\n}\n\n/**\n * The routed view of a collection DOCUMENT (AGL-551): its name and the\n * template screens its list and entry routes render through.\n *\n * `slug` is the slug that was ASKED FOR rather than the one stored, which is\n * what every caller of this file has always returned — a listing has to build\n * its own URLs out of the segment the reader is standing on.\n */\nfunction mapCollectionDoc(\n collectionDoc: FirebaseFirestore.QueryDocumentSnapshot,\n collectionSlug: string,\n): CollectionContent['collection'] {\n return {\n $id: collectionDoc.id,\n displayName: collectionDoc.get('displayName') ?? collectionSlug,\n slug: collectionSlug,\n templateScreenId: collectionDoc.get('templateScreenId') ?? undefined,\n listScreenId: collectionDoc.get('listScreenId') ?? undefined,\n entryScreenId: collectionDoc.get('entryScreenId') ?? undefined,\n // Normalized HERE rather than at the head (AGL-2536), so an unrecognised\n // stored value can never reach the JSON-LD: an `@type` the vocabulary\n // does not define makes a consumer discard the whole node, costing the\n // page every property it publishes rather than just this one.\n schemaType: normalizeContentSchemaType(collectionDoc.get('schemaType')),\n categories: mapCollectionCategories(collectionDoc.get('categories')),\n }\n}\n\n/**\n * Resolve the author RECORDS a set of entries reference (AGL-2486).\n *\n * Costs ZERO reads when no entry names an `authorId`, which is every site\n * that has not adopted custom authors and every entry written before them —\n * the check is on the ids already in hand, not a probe of the collection. When\n * ids are present it is one `getAll` of the DISTINCT ones, bounded by\n * {@link AUTHORS_MAX_PER_HOST} and by the ≤100-entry page above it, rather\n * than a read per entry.\n *\n * Fail-open, like every other read in this file: an authors read that throws\n * leaves the entries with their legacy `authorName` (or the site entity) and\n * the page renders. A byline is not worth a 500.\n */\nasync function attachEntryAuthors(\n hostId: string,\n entries: CollectionEntrySummary[],\n): Promise<void> {\n const ids = [\n ...new Set(\n entries\n .map((entry) => (entry.authorId ?? '').trim())\n .filter(Boolean),\n ),\n ].slice(0, AUTHORS_MAX_PER_HOST)\n let authors: ContentAuthorRecord[] = []\n if (ids.length) {\n try {\n const authorsRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('authors')\n const snapshots = await firebaseAdmin\n .app()\n .firestore()\n .getAll(...ids.map((id) => authorsRef.doc(id)))\n authors = snapshots\n .map((snapshot) =>\n snapshot.exists\n ? normalizeContentAuthor(snapshot.data(), snapshot.id)\n : null,\n )\n .filter((author): author is ContentAuthorRecord => Boolean(author))\n } catch (error) {\n console.error(error)\n }\n }\n for (const entry of entries) {\n const author = resolveEntryAuthor(entry, authors)\n entry.author = author\n // The byline TEXT is denormalized onto the field everything downstream\n // already reads, so a record-backed author needs no change in the Entry\n // Meta block, the token map, the fallback nodes or the RSS feed. A record\n // WINS over the legacy string on the same entry: picking an author is the\n // more recent statement of who wrote it.\n if (author?.name) entry.authorName = author.name\n }\n}\n\n/**\n * Terminal refusal marker for an entry schedule, the flat-status twin of\n * `publishSchedule.status: 'skipped-unentitled'` on screens (AGL-1185).\n *\n * A field rather than a new `status` value on purpose. `status` is queried\n * (`where('status', 'in', [...])`), rendered by the console, and sorted on by\n * `bundle-timestamps.ts`; adding a member would have meant auditing every one\n * of those readers. A sibling field is invisible to all of them and is read\n * only here.\n */\nconst ENTRY_SCHEDULE_SKIPPED = 'skipped-unentitled'\n\n/** A schedule this entry already had declined, and will not have reconsidered. */\nfunction scheduleAlreadyRefused(\n value: FirebaseFirestore.DocumentData,\n): boolean {\n return value['scheduleStatus'] === ENTRY_SCHEDULE_SKIPPED\n}\n\n/** A scheduled entry whose time has come — before any plan question. */\nexport function isDueScheduled(value: FirebaseFirestore.DocumentData): boolean {\n return (\n value['status'] === 'scheduled' &&\n !scheduleAlreadyRefused(value) &&\n (value['publishAt']?.seconds ?? Number.POSITIVE_INFINITY) * 1000 <=\n Date.now()\n )\n}\n\n/**\n * A scheduled entry still waiting on its time: not due yet, and not refused.\n * Nothing but a render publishes a content entry, so a cache that stored a\n * read holding one would withhold the render that notices it come due — see\n * {@link LiveEntriesRead.pendingSchedule}.\n */\nexport function isPendingScheduled(\n value: FirebaseFirestore.DocumentData,\n): boolean {\n return (\n value['status'] === 'scheduled' &&\n !scheduleAlreadyRefused(value) &&\n !isDueScheduled(value)\n )\n}\n\n/**\n * Is this host's plan allowed to publish on a schedule? (AGL-471 shape.)\n *\n * `React.cache`-deduped per request via `getOrgForHost`, and — this is the\n * part that keeps it off the hot path — every caller below only asks once it\n * has already found a due scheduled entry. A collection with nothing due pays\n * nothing, which is almost every render.\n *\n * THREE answers, not two, and the third is the point. `refused` means we read\n * the plan and it does not carry the entitlement. `unresolved` means we could\n * not find out. Both withhold the entry, but only `refused` may write the\n * terminal marker — burning a schedule permanently on the strength of a\n * hostIndex miss or a transient rejection would destroy a customer's post for\n * a reason that may not be true a second later.\n *\n * Withholding on `unresolved` rather than publishing is what every other\n * entitlement caller on the tenant runtime already does: `apply-publish-schedule`\n * here, and the automation engine's two runners, all pass a possibly\n * undefined org straight into `checkEntitlement`, which resolves a missing\n * plan as free and denies (AGL-247). Opening here instead would make this the\n * one gate in the lib that admits when it cannot see — the exact shape of the\n * free-tier leak `no-plan-gated-entitlement` exists to forbid.\n *\n * The blast radius of withholding is deliberately small: `isLive` answers true\n * for `status: 'published'` before it ever consults this, so an unresolved\n * read hides only the due-scheduled entry, never the published ones, and the\n * next render retries.\n */\nexport type SchedulePermission = 'allowed' | 'refused' | 'unresolved'\n\nexport async function scheduledPublishingPermission(\n hostId: string,\n): Promise<SchedulePermission> {\n try {\n const org = (await getOrgForHost(hostId))?.org\n if (!org) return 'unresolved'\n return checkEntitlement(org, 'scheduledPublishing') ? 'allowed' : 'refused'\n } catch (error) {\n // Caught rather than thrown: the only caller sits inside\n // `getCollectionContent`'s try/catch, which returns an EMPTY collection —\n // so an unhandled rejection here would blank every published entry on the\n // page, not just the scheduled one.\n console.error(error)\n return 'unresolved'\n }\n}\n\n/**\n * Scheduled entries (AGL-123) go live lazily like AGL-61: a due\n * `publishAt` counts as published for this render, and the doc is flipped\n * to `published` fail-open so the state becomes durable.\n *\n * PLAN GATE (AGL-471). `scheduledPublishing` is a Business entitlement, and\n * until now nothing on the entry path checked it: the console let any plan\n * write `status: 'scheduled'`, and this render path published it. Scheduling\n * worked end to end on Free. The screens path has gated this since AGL-471\n * and records its refusal since AGL-1185 — entries were simply never wired\n * to either, which is why the leak was invisible from the screens side.\n *\n * The permission is threaded in rather than resolved here so the org read\n * happens once per call site instead of once per entry.\n *\n * Exported for the one other reader that decides whether an entry is on the\n * site — whether a LINK to it resolves (AGL-3118) — so a link and the page it\n * points at can never disagree about which entries exist.\n */\nexport function isLive(\n value: FirebaseFirestore.DocumentData,\n permission: SchedulePermission,\n): boolean {\n if (value['status'] === 'published') return true\n return permission === 'allowed' && isDueScheduled(value)\n}\n\n/**\n * Make the due state durable — or record that it was refused.\n *\n * The refusal is TERMINAL, for the AGL-1185 reason: left as a bare pending\n * `scheduled`, the entry stays permanently due, so the day the org upgrades\n * to Business the next render publishes it. Content scheduled on a plan that\n * could not honour it, and forgotten, surfacing during an upgrade — exactly\n * when nobody is looking for it. Recording the refusal is what makes it stop\n * being due, and it also stops this path re-reading the org on every\n * subsequent render.\n *\n * Both writes fail open: an error leaves today's state, and the next render\n * retries.\n */\nfunction flipDueEntry(\n docRef: FirebaseFirestore.DocumentReference,\n value: FirebaseFirestore.DocumentData,\n permission: SchedulePermission,\n): void {\n if (value['status'] !== 'scheduled') return\n if (scheduleAlreadyRefused(value)) return\n // `unresolved` writes NOTHING. It withholds this render and leaves the\n // schedule exactly as it found it, so a later render can still publish it.\n if (permission === 'unresolved') return\n if (permission === 'refused') {\n if (!isDueScheduled(value)) return\n docRef\n .update({ scheduleStatus: ENTRY_SCHEDULE_SKIPPED })\n .catch((error) => console.error(error))\n return\n }\n docRef\n .update({ status: 'published', publishedAt: value['publishAt'] })\n .catch((error) => console.error(error))\n}\n\nexport interface CollectionContent {\n collection: {\n $id: string\n displayName: string\n slug: string\n /**\n * Legacy entry-template screen (AGL-105); superseded by\n * `entryScreenId` but still honored when only it is set.\n */\n templateScreenId?: string\n /** List-template screen (AGL-551); `/{collection}` renders through it. */\n listScreenId?: string\n /**\n * Entry-template screen (AGL-551); `/{collection}/{entry}` renders\n * through it with `{{entry.*}}` tokens.\n */\n entryScreenId?: string\n /**\n * What KIND of article this collection publishes (AGL-2536) — the\n * `schema.org` type its entries serialise as. Unset publishes `Article`,\n * which is what every collection published before the setting existed.\n */\n schemaType?: string\n /**\n * Category taxonomy (AGL-582): entries reference these by stable\n * `id`; `name` is the renameable display label.\n */\n categories?: CollectionCategory[]\n } | null\n entries: CollectionEntrySummary[]\n entry: CollectionEntrySummary | null\n /**\n * Whether the read that produced `entries` stopped at\n * {@link COLLECTION_SOURCE_MAX} (AGL-1516). Set on LIST routes only —\n * an entry route reads one document by slug and bounds nothing.\n */\n entriesReachedBound?: boolean\n /** List pagination (AGL-620); null for entry pages or unpaginated lists. */\n pagination?: CollectionPagination | null\n /**\n * The category this listing is filtered to (AGL-1321); null on the\n * canonical unfiltered list and on entry pages.\n */\n category?: CollectionRouteCategory | null\n error: unknown\n}\n\n/** The category a `/{collection}/category/{slug}` route addresses (AGL-1321). */\nexport interface CollectionRouteCategory {\n /** The URL segment, normalized — what the canonical link must say. */\n slug: string\n /** Taxonomy id; absent when the segment matched no known category. */\n id?: string\n /** Display label; falls back to the raw segment for an unknown category. */\n name: string\n /**\n * The taxonomy's {@link CollectionCategory.description}, carried onto the\n * route so the head can describe the FILTERED listing rather than inherit\n * the whole collection's description. Absent for an unknown segment, which\n * names no category and therefore has nothing to describe.\n */\n description?: string\n /**\n * Whether the segment resolved against the collection's taxonomy. An\n * unknown category still renders — an empty listing, not a crash — but the\n * page must not invite indexing of a URL that names nothing.\n */\n known: boolean\n}\n\nexport interface CollectionPagination {\n /** 1-based current page. */\n page: number\n perPage: number\n totalPages: number\n totalEntries: number\n}\n\n/**\n * A bounded read of a collection's live entries (AGL-1516).\n *\n * `reachedBound` is a fact about the QUERY, not about `entries`, and the two\n * genuinely differ: the query asks for `status in ['published', 'scheduled']`\n * and the filter below then drops everything not live yet, so a read that came\n * back holding all {@link COLLECTION_SOURCE_MAX} docs can hand back fewer.\n * Counting the survivors — which is all a downstream consumer can do — reads\n * that as a complete collection, and the one thing a truncated read must never\n * be allowed to claim is completeness.\n */\ninterface LiveEntriesRead {\n entries: CollectionEntrySummary[]\n /** The query came back holding its own `.limit()`. */\n reachedBound: boolean\n /**\n * The read saw a `scheduled` entry whose `publishAt` has NOT arrived and\n * which has not been terminally refused — a schedule this collection is\n * still waiting on.\n *\n * Nothing promotes a content entry on a beat: `publish-schedule-job.ts` is\n * screens-only, so `isLive`/`flipDueEntry` running during a render is the\n * entire mechanism. A cached source therefore does not merely serve stale\n * entries, it withholds the render that would have published one, for as\n * long as the entry stays cached. This is what lets the cache decline to\n * store exactly those collections, so a schedule keeps landing on the\n * render window rather than on the TTL.\n */\n pendingSchedule: boolean\n}\n\n/**\n * Fetches a collection's live entries (newest first), shared by the route\n * loader and the compose-time Collection entries block (AGL-551).\n */\nasync function listLiveEntries(\n entriesRef: FirebaseFirestore.CollectionReference,\n hostId: string,\n): Promise<LiveEntriesRead> {\n // No orderBy: entries missing publishedAt would be dropped by Firestore;\n // sort client-side like the version lists.\n const entriesQuery = await entriesRef\n .where('status', 'in', ['published', 'scheduled'])\n // Named rather than literal (AGL-1516): a search index has to be able to\n // say \"this read reached its bound\", and it can only do that against a\n // bound it shares with the query. `collectionSourceReachedBound` reads\n // the same constant.\n .limit(COLLECTION_SOURCE_MAX)\n .get()\n\n // Ask the plan question ONLY if something is actually due (AGL-471). A\n // collection with no due schedule — almost every render — never reads the\n // org at all.\n const due = entriesQuery.docs.filter((entryDoc) =>\n isDueScheduled(entryDoc.data()),\n )\n const permission: SchedulePermission = due.length\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n\n // Record the terminal refusal on its own pass, because a refused entry is\n // NOT live and so never reaches the `flipDueEntry` inside the map below.\n // Without this the entry stays due forever: excluded from every render, and\n // re-reading the org on each one.\n if (permission !== 'allowed') {\n for (const entryDoc of due) {\n flipDueEntry(entryDoc.ref, entryDoc.data(), permission)\n }\n }\n\n // Measured on the RAW docs, before the liveness filter (AGL-1516). This is\n // the only place that can still see how many documents the query returned;\n // one line down that number is gone for good.\n const reachedBound = entriesQuery.docs.length >= COLLECTION_SOURCE_MAX\n\n // Also measured on the RAW docs, and for the same reason: a not-yet-due\n // entry is filtered out one line down, so this is the last place that can\n // see one at all.\n const pendingSchedule = entriesQuery.docs.some((entryDoc) =>\n isPendingScheduled(entryDoc.data()),\n )\n\n const entries = entriesQuery.docs\n .filter((entryDoc) => isLive(entryDoc.data(), permission))\n .map((entryDoc) => {\n const value = entryDoc.data()\n flipDueEntry(entryDoc.ref, value, permission)\n return {\n $id: entryDoc.id,\n title: value['title'] ?? entryDoc.id,\n slug: value['slug'] ?? entryDoc.id,\n ...mapEntryFields(value),\n publishedAt: (value['publishedAt'] ?? value['publishAt'])\n ? {\n seconds: (value['publishedAt'] ?? value['publishAt']).seconds,\n }\n : null,\n }\n })\n .sort(\n (a, b) => (b.publishedAt?.seconds ?? 0) - (a.publishedAt?.seconds ?? 0),\n )\n\n return { entries, reachedBound, pendingSchedule }\n}\n\n/** Compose-time view of a collection: its live entries and its taxonomy. */\nexport interface PublishedCollectionSource {\n /**\n * The collection DOCUMENT this source was read from — its display name and\n * its template screen ids — or null when the slug names no content\n * collection.\n *\n * Carried so a routed listing can be served ENTIRELY from this cached\n * source. `getCollectionContent` used to resolve the same document itself\n * and then read the same entries again uncached, which meant `/blog`,\n * every `/blog/page/{n}`, every `/blog/category/{slug}` and the RSS feed\n * each paid a full collection read per regeneration while the identical\n * data already sat in this cache for every OTHER page on the site.\n */\n collection: CollectionContent['collection']\n entries: CollectionEntrySummary[]\n categories: CollectionCategory[]\n /**\n * Whether the read saw a schedule it is still waiting on — see\n * {@link LiveEntriesRead.pendingSchedule}. Never stored, only consulted by\n * the cache above, so no consumer has to know about it.\n */\n pendingSchedule?: boolean\n /**\n * Whether the entries read stopped at {@link COLLECTION_SOURCE_MAX}\n * (AGL-1516) — carried out of the loader because `entries.length` cannot\n * answer it once the liveness filter has run. Fail-open paths report\n * `false`: an empty result is not a bounded read, and describing it as one\n * would tell a reader their search covered less than it did.\n */\n reachedBound: boolean\n}\n\n/**\n * Published entries + category taxonomy for a collection resolved by slug —\n * the data source of the Collection entries block on arbitrary screens\n * (AGL-551/582). Fail-open: errors and unknown slugs resolve to an empty\n * list so a renamed collection never takes a published screen down.\n */\nexport async function getPublishedCollectionSource(options: {\n hostId: string\n collectionSlug: string\n}): Promise<PublishedCollectionSource> {\n try {\n return await withRenderCache({\n key: [\n 'tenant-collection-source',\n options.hostId,\n options.collectionSlug,\n ],\n revalidate: COLLECTION_SOURCE_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readPublishedCollectionSource(options),\n // A collection with a schedule still pending is SERVED but not STORED.\n //\n // No beat publishes a content entry — `flipDueEntry` during a render is\n // the whole mechanism — so storing this source would suppress the very\n // renders that would have noticed the entry coming due, and the post\n // would wait out the TTL rather than land at its time. Declining to\n // store leaves those collections exactly as uncached as they were\n // before this cache existed, which is the only cost this cache is not\n // allowed to reduce.\n //\n // Also refuses a collection that resolved to nothing, for the reason\n // `withRenderCache` states about negatives generally: a slug that\n // misses once must not miss for an hour.\n store: (value) => Boolean(value.collection) && !value.pendingSchedule,\n })\n } catch (error) {\n console.error(error)\n return readPublishedCollectionSource(options)\n }\n}\n\nasync function readPublishedCollectionSource(\n options: {\n hostId: string\n collectionSlug: string\n },\n): Promise<PublishedCollectionSource> {\n try {\n const collectionDoc = await findContentCollection(\n options.hostId,\n options.collectionSlug,\n )\n if (!collectionDoc) {\n return {\n collection: null,\n entries: [],\n categories: [],\n reachedBound: false,\n }\n }\n const { entries, reachedBound, pendingSchedule } = await listLiveEntries(\n collectionDoc.ref.collection('entries'),\n options.hostId,\n )\n // The compose-time source feeds the Collection entries block, whose byline\n // reads `authorName` — so a record-backed author has to be resolved here\n // too, or the block prints nothing for the entries a list page shows\n // (AGL-2486). This result is the cached one, which is what keeps the extra\n // read amortized across every page of the site that carries the block.\n await attachEntryAuthors(options.hostId, entries)\n return {\n collection: mapCollectionDoc(collectionDoc, options.collectionSlug),\n entries,\n categories: mapCollectionCategories(collectionDoc.get('categories')),\n reachedBound,\n ...(pendingSchedule ? { pendingSchedule: true } : {}),\n }\n } catch (error) {\n console.error(error)\n return {\n collection: null,\n entries: [],\n categories: [],\n reachedBound: false,\n }\n }\n}\n\n/** Entries-only view of {@link getPublishedCollectionSource} (AGL-551). */\nexport async function getPublishedCollectionEntries(options: {\n hostId: string\n collectionSlug: string\n}): Promise<CollectionEntrySummary[]> {\n return (await getPublishedCollectionSource(options)).entries\n}\n\n/**\n * Narrows a listing to its routed category and stamps its pagination\n * (AGL-1321 / AGL-620).\n *\n * Category first, ALWAYS: the two have to describe the same set. Counting\n * pages over the whole collection and then filtering would advertise pages\n * that render empty and hide entries that exist.\n *\n * `entriesReachedBound` is read by the caller BEFORE this runs, because a\n * category route hands its already-narrowed entries to compose and the\n * entries block's \"measure the raw set, not the filtered one\" rule then has\n * nothing raw left to measure (AGL-1516).\n */\nfunction applyCategoryAndPagination(\n data: CollectionContent,\n options: {\n page?: number\n perPage?: number\n categorySlug?: string\n },\n): void {\n const { page = 1, perPage } = options\n const routedCategory = (options.categorySlug ?? '').trim()\n if (routedCategory) {\n const match = resolveCollectionCategoryBySlug(\n data.collection?.categories,\n routedCategory,\n )\n data.category = {\n slug: collectionCategorySlug(routedCategory),\n ...(match ? { id: match.id } : {}),\n name: match?.name ?? routedCategory,\n ...(match?.description ? { description: match.description } : {}),\n known: Boolean(match),\n }\n data.entries = data.entries.filter((entry) =>\n entryMatchesCategoryRoute(\n entry,\n { slug: routedCategory, ...(match ? { category: match } : {}) },\n data.collection?.categories,\n ),\n )\n }\n\n if (perPage && perPage > 0) {\n const totalEntries = data.entries.length\n data.pagination = {\n page,\n perPage,\n totalEntries,\n totalPages: collectionTotalPages(totalEntries, perPage),\n }\n }\n}\n\n/**\n * Resolves a non-screen path against the host's content collections\n * (Content Collections & Blog): `/{collectionSlug}` returns the published\n * entry list, `/{collectionSlug}/{entrySlug}` one entry. Fail-open — errors\n * resolve to `collection: null` and the caller 404s.\n *\n * A listing resolves only for a collection with a live entry (AGL-3101); one\n * with nothing live answers `collection: null` as well, so its listing, feed\n * and markdown twin are not public until its first entry is.\n */\nexport async function getCollectionContent(options: {\n hostId: string\n collectionSlug: string\n entrySlug?: string\n /** 1-based list page (AGL-620); with `perPage`, drives pagination metadata. */\n page?: number\n /** Entries per page (AGL-620); when set the list is paginated. */\n perPage?: number\n /**\n * Category segment of `/{collection}/category/{slug}` (AGL-1321). Filters\n * the listing before pagination is computed, so page counts and the page\n * windows describe the FILTERED set rather than the whole collection.\n */\n categorySlug?: string\n}): Promise<CollectionContent> {\n const { hostId, collectionSlug, entrySlug } = options\n const data: CollectionContent = {\n collection: null,\n entries: [],\n entry: null,\n pagination: null,\n category: null,\n error: null,\n }\n try {\n // A LIST route is served entirely from the CACHED source, which every\n // other page on the site already shares. It used to resolve the\n // collection and re-read its entries here, uncached, on every\n // regeneration of every listing address — `/blog`, nine `/blog/page/{n}`,\n // one `/blog/category/{slug}` per category, and the RSS feed — so a\n // collection of N live entries cost `1 + N + authors` reads per address\n // per window, for data byte-identical to what the cache was already\n // holding for the home page's \"Latest posts\" rail.\n //\n // An ENTRY route stays where it was: it reads ONE document by slug and\n // has nothing to share.\n if (!entrySlug) {\n const source = await getPublishedCollectionSource({\n hostId,\n collectionSlug,\n })\n if (!source.collection) return data\n // A collection is not a PAGE until something in it is live (AGL-3101).\n // Without this, creating one publishes `/{slug}` at once — an empty\n // listing with a feed and a markdown twin, before a word of it is\n // written. Answering it as no collection makes every listing address\n // 404 the way an unknown slug does, until the first entry is published\n // or its schedule comes due.\n //\n // Read off the UNFILTERED live set, before the category narrows it, so\n // an empty category of a live collection still renders. A read that\n // stopped at its bound cannot prove the collection empty — the live\n // entries may be past it — so that one keeps its listing. The rule\n // lives here and not in the source above: the source also feeds the\n // Collection entries block and the author page, and to them an empty\n // collection is an empty list, not a missing one.\n if (!source.entries.length && !source.reachedBound) return data\n data.collection = source.collection\n data.entries = source.entries\n data.entriesReachedBound = source.reachedBound\n applyCategoryAndPagination(data, options)\n return data\n }\n\n const collectionDoc = await findContentCollection(hostId, collectionSlug)\n if (!collectionDoc) return data\n data.collection = mapCollectionDoc(collectionDoc, collectionSlug)\n\n const entryQuery = await collectionDoc.ref\n .collection('entries')\n .where('slug', '==', entrySlug)\n .limit(5)\n .get()\n // Same two-step as `listLiveEntries`: only pay for the org read when\n // something is due, and record the refusal on its own pass because a\n // refused entry never becomes the `entryDoc` below (AGL-471).\n const dueHere = entryQuery.docs.filter((docSnapshot) =>\n isDueScheduled(docSnapshot.data()),\n )\n const permission: SchedulePermission = dueHere.length\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n if (permission !== 'allowed') {\n for (const docSnapshot of dueHere) {\n flipDueEntry(docSnapshot.ref, docSnapshot.data(), permission)\n }\n }\n const entryDoc = entryQuery.docs.find((docSnapshot) =>\n isLive(docSnapshot.data(), permission),\n )\n if (entryDoc) {\n const value = entryDoc.data()\n flipDueEntry(entryDoc.ref, value, permission)\n data.entry = {\n $id: entryDoc.id,\n title: value['title'] ?? entrySlug,\n slug: entrySlug,\n body: value['body'] ?? '',\n ...mapEntryFields(value),\n publishedAt: (value['publishedAt'] ?? value['publishAt'])\n ? {\n seconds: (value['publishedAt'] ?? value['publishAt']).seconds,\n }\n : null,\n }\n await attachEntryAuthors(hostId, [data.entry])\n }\n } catch (error) {\n console.error(error)\n data.error = error\n }\n return data\n}\n\nexport default getCollectionContent\n"],"names":["AUTHORS_MAX_PER_HOST","collectionCategorySlug","checkEntitlement","COLLECTION_SOURCE_MAX","collectionTotalPages","entryMatchesCategoryRoute","hostCollectionKind","normalizeContentAuthor","normalizeContentSchemaType","resolveCollectionCategoryBySlug","resolveEntryAuthor","firebaseAdmin","getOrgForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","COLLECTION_SOURCE_TTL_SECONDS","findContentCollection","hostId","collectionSlug","matches","app","firestore","collection","doc","where","limit","get","docs","find","docSnapshot","data","mapEntryFields","value","excerpt","authorName","authorId","coverImage","coverImageAlt","coverVideo","seoTitle","seoDescription","categoryId","category","tags","Array","isArray","filter","tag","updatedAt","seconds","mapCollectionCategories","item","id","trim","name","map","description","mapCollectionDoc","collectionDoc","$id","displayName","slug","templateScreenId","undefined","listScreenId","entryScreenId","schemaType","categories","attachEntryAuthors","entries","ids","Set","entry","Boolean","slice","authors","length","authorsRef","snapshots","getAll","snapshot","exists","author","error","console","ENTRY_SCHEDULE_SKIPPED","scheduleAlreadyRefused","isDueScheduled","Number","POSITIVE_INFINITY","Date","now","isPendingScheduled","scheduledPublishingPermission","org","isLive","permission","flipDueEntry","docRef","update","scheduleStatus","catch","status","publishedAt","listLiveEntries","entriesRef","entriesQuery","due","entryDoc","ref","reachedBound","pendingSchedule","some","title","sort","a","b","getPublishedCollectionSource","options","key","revalidate","read","readPublishedCollectionSource","store","getPublishedCollectionEntries","applyCategoryAndPagination","page","perPage","routedCategory","categorySlug","match","known","totalEntries","pagination","totalPages","getCollectionContent","entrySlug","source","entriesReachedBound","entryQuery","dueHere","body"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as Aglyn from '@aglyn/aglyn/server';
18
+ export declare function getComponents(options: {
19
+ hostId: Aglyn.HostUid;
20
+ }): Promise<{
21
+ definitions: Record<string, Aglyn.ReusableComponentTree>;
22
+ error: unknown;
23
+ }>;
24
+ export default getComponents;
@@ -0,0 +1,102 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import * as Aglyn from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
20
+ /**
21
+ * Part of the per-host compose bundle every page render pays (AGL-1302):
22
+ * up to 200 docs per render before caching. Component publishes announce
23
+ * themselves through the console revalidate route, which busts
24
+ * `tenant-data:{hostId}`; The TTL is only the backstop for writes that never announce themselves — see PUBLISHED_SITE_DATA_TTL_SECONDS.
25
+ */ const COMPONENTS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
26
+ /**
27
+ * Fetches the host's reusable component definitions keyed by id, in the
28
+ * shape `composeReusableComponentNodes` consumes. Fail-open: on error an
29
+ * empty map is returned (instances render as empty wrappers, the page still
30
+ * serves).
31
+ */ async function readComponents(hostId) {
32
+ const data = {
33
+ definitions: {},
34
+ error: null
35
+ };
36
+ const firestore = firebaseAdmin.app().firestore();
37
+ await firestore.collection('hosts').doc(hostId).collection('components').limit(200).get().then((res)=>{
38
+ for (const docSnapshot of res.docs){
39
+ var _value_props;
40
+ const value = docSnapshot.data();
41
+ if ((value == null ? void 0 : value.deletedAt) || !(value == null ? void 0 : value.nodes) || !(value == null ? void 0 : value.rootId)) continue;
42
+ /*
43
+ * BOTH STORED FORMS, on the hot path (AGL-1151).
44
+ *
45
+ * A published definition is msgpack for anything promoted since
46
+ * components were compressed and a plain map for everything older,
47
+ * and nothing migrates them — `decodeStoredNodes` returns a map
48
+ * unchanged, so one call serves both forever.
49
+ *
50
+ * It has to be here rather than at the caller because the failure is
51
+ * silent and cached: `composeReusableComponentNodes` looks up
52
+ * `nodes[rootId]` on the value below, finds nothing in a `Buffer`,
53
+ * and grafts an empty wrapper — so every instance of the component
54
+ * disappears from every page of the site, and the result is stored
55
+ * under the render cache for the rest of its TTL.
56
+ *
57
+ * The decode costs no new dependency: this module already imports
58
+ * `@aglyn/aglyn/server`, and the tenant's published-page CLIENT
59
+ * bundle never reaches this file.
60
+ */ const nodes = Aglyn.decodeStoredNodes(value.nodes);
61
+ // An undecodable definition is skipped rather than grafted empty. It
62
+ // is the same outcome for the page either way, but `decodeStoredNodes`
63
+ // logs the reason, and a definition that silently became `{}` would
64
+ // not say why the component vanished.
65
+ if (!nodes) continue;
66
+ data.definitions[docSnapshot.id] = _extends({
67
+ rootId: value.rootId,
68
+ nodes
69
+ }, ((_value_props = value.props) == null ? void 0 : _value_props.length) && {
70
+ props: value.props
71
+ });
72
+ }
73
+ }).catch((error)=>{
74
+ console.error(error);
75
+ data.error = error;
76
+ });
77
+ return data;
78
+ }
79
+ export async function getComponents(options) {
80
+ const { hostId } = options;
81
+ try {
82
+ return await withRenderCache({
83
+ key: [
84
+ 'tenant-components',
85
+ hostId
86
+ ],
87
+ revalidate: COMPONENTS_TTL_SECONDS,
88
+ tags: [
89
+ tenantDataTag(hostId)
90
+ ],
91
+ read: ()=>readComponents(hostId),
92
+ // A failed read keeps its fail-open empty map per-request only.
93
+ store: (value)=>!value.error
94
+ });
95
+ } catch (error) {
96
+ console.error(error);
97
+ return readComponents(hostId);
98
+ }
99
+ }
100
+ export default getComponents;
101
+
102
+ //# sourceMappingURL=get-components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-components.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * Part of the per-host compose bundle every page render pays (AGL-1302):\n * up to 200 docs per render before caching. Component publishes announce\n * themselves through the console revalidate route, which busts\n * `tenant-data:{hostId}`; The TTL is only the backstop for writes that never announce themselves — see PUBLISHED_SITE_DATA_TTL_SECONDS.\n */\nconst COMPONENTS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * Fetches the host's reusable component definitions keyed by id, in the\n * shape `composeReusableComponentNodes` consumes. Fail-open: on error an\n * empty map is returned (instances render as empty wrappers, the page still\n * serves).\n */\nasync function readComponents(hostId: Aglyn.HostUid) {\n const data = {\n definitions: {} as Record<string, Aglyn.ReusableComponentTree>,\n error: null as unknown,\n }\n const firestore = firebaseAdmin.app().firestore()\n\n await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('components')\n .limit(200)\n .get()\n .then((res) => {\n for (const docSnapshot of res.docs) {\n const value = docSnapshot.data() as Aglyn.AglynHostComponent\n if (value?.deletedAt || !value?.nodes || !value?.rootId) continue\n /*\n * BOTH STORED FORMS, on the hot path (AGL-1151).\n *\n * A published definition is msgpack for anything promoted since\n * components were compressed and a plain map for everything older,\n * and nothing migrates them — `decodeStoredNodes` returns a map\n * unchanged, so one call serves both forever.\n *\n * It has to be here rather than at the caller because the failure is\n * silent and cached: `composeReusableComponentNodes` looks up\n * `nodes[rootId]` on the value below, finds nothing in a `Buffer`,\n * and grafts an empty wrapper — so every instance of the component\n * disappears from every page of the site, and the result is stored\n * under the render cache for the rest of its TTL.\n *\n * The decode costs no new dependency: this module already imports\n * `@aglyn/aglyn/server`, and the tenant's published-page CLIENT\n * bundle never reaches this file.\n */\n const nodes = Aglyn.decodeStoredNodes<\n Aglyn.ReusableComponentTree['nodes']\n >(value.nodes)\n // An undecodable definition is skipped rather than grafted empty. It\n // is the same outcome for the page either way, but `decodeStoredNodes`\n // logs the reason, and a definition that silently became `{}` would\n // not say why the component vanished.\n if (!nodes) continue\n data.definitions[docSnapshot.id] = {\n rootId: value.rootId,\n nodes,\n // Declared props (AGL-1247): without these the graft leaves every\n // `{{prop.*}}` token unresolved on the published page.\n ...(value.props?.length && { props: value.props }),\n }\n }\n })\n .catch((error) => {\n console.error(error)\n data.error = error\n })\n\n return data\n}\n\nexport async function getComponents(options: { hostId: Aglyn.HostUid }) {\n const { hostId } = options\n try {\n return await withRenderCache({\n key: ['tenant-components', hostId as string],\n revalidate: COMPONENTS_TTL_SECONDS,\n tags: [tenantDataTag(hostId as string)],\n read: () => readComponents(hostId),\n // A failed read keeps its fail-open empty map per-request only.\n store: (value) => !value.error,\n })\n } catch (error) {\n console.error(error)\n return readComponents(hostId)\n }\n}\n\nexport default getComponents\n"],"names":["Aglyn","firebaseAdmin","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","COMPONENTS_TTL_SECONDS","readComponents","hostId","data","definitions","error","firestore","app","collection","doc","limit","get","then","res","docSnapshot","docs","value","deletedAt","nodes","rootId","decodeStoredNodes","id","props","length","catch","console","getComponents","options","key","revalidate","tags","read","store"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAE9C;;;;;CAKC,GACD,MAAMC,yBAAyBH;AAE/B;;;;;CAKC,GACD,eAAeI,eAAeC,MAAqB;IACjD,MAAMC,OAAO;QACXC,aAAa,CAAC;QACdC,OAAO;IACT;IACA,MAAMC,YAAYV,cAAcW,GAAG,GAAGD,SAAS;IAE/C,MAAMA,UACHE,UAAU,CAAC,SACXC,GAAG,CAACP,QACJM,UAAU,CAAC,cACXE,KAAK,CAAC,KACNC,GAAG,GACHC,IAAI,CAAC,CAACC;QACL,KAAK,MAAMC,eAAeD,IAAIE,IAAI,CAAE;gBAmC5BC;YAlCN,MAAMA,QAAQF,YAAYX,IAAI;YAC9B,IAAIa,CAAAA,yBAAAA,MAAOC,SAAS,KAAI,EAACD,yBAAAA,MAAOE,KAAK,KAAI,EAACF,yBAAAA,MAAOG,MAAM,GAAE;YACzD;;;;;;;;;;;;;;;;;;SAkBC,GACD,MAAMD,QAAQvB,MAAMyB,iBAAiB,CAEnCJ,MAAME,KAAK;YACb,qEAAqE;YACrE,uEAAuE;YACvE,oEAAoE;YACpE,sCAAsC;YACtC,IAAI,CAACA,OAAO;YACZf,KAAKC,WAAW,CAACU,YAAYO,EAAE,CAAC,GAAG;gBACjCF,QAAQH,MAAMG,MAAM;gBACpBD;eAGIF,EAAAA,eAAAA,MAAMM,KAAK,qBAAXN,aAAaO,MAAM,KAAI;gBAAED,OAAON,MAAMM,KAAK;YAAC;QAEpD;IACF,GACCE,KAAK,CAAC,CAACnB;QACNoB,QAAQpB,KAAK,CAACA;QACdF,KAAKE,KAAK,GAAGA;IACf;IAEF,OAAOF;AACT;AAEA,OAAO,eAAeuB,cAAcC,OAAkC;IACpE,MAAM,EAAEzB,MAAM,EAAE,GAAGyB;IACnB,IAAI;QACF,OAAO,MAAM5B,gBAAgB;YAC3B6B,KAAK;gBAAC;gBAAqB1B;aAAiB;YAC5C2B,YAAY7B;YACZ8B,MAAM;gBAAChC,cAAcI;aAAkB;YACvC6B,MAAM,IAAM9B,eAAeC;YAC3B,gEAAgE;YAChE8B,OAAO,CAAChB,QAAU,CAACA,MAAMX,KAAK;QAChC;IACF,EAAE,OAAOA,OAAO;QACdoB,QAAQpB,KAAK,CAACA;QACd,OAAOJ,eAAeC;IACxB;AACF;AAEA,eAAewB,cAAa"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as Aglyn from '@aglyn/aglyn/server';
18
+ /**
19
+ * The datasets a page repeats over, with their records, for repeatable
20
+ * expansion (AGL-103).
21
+ *
22
+ * `keys` are the page's repeat keys (`repeatDatasetKeys`), each a dataset id or
23
+ * a display name — editors type the friendly name into the Repeat attribute.
24
+ * The map answers under every key exactly as written, and under every loaded
25
+ * dataset's id, which is how a reference hop (AGL-180) finds its target.
26
+ * Records are editor-ordered and capped at the repeat bound.
27
+ *
28
+ * Only the datasets the page names are read. A site may see thousands of
29
+ * datasets, and a read of "the site's datasets" can only ever be a bounded
30
+ * page of them, so a repeat bound to one outside that page rendered its
31
+ * template once with nothing to say why.
32
+ *
33
+ * Fail-open, per key: a key that cannot be read is left out, and its
34
+ * repeatable renders its template untouched, without costing the others.
35
+ *
36
+ * Scoped to what THIS host may see (AGL-1039): a display name resolves inside
37
+ * the host's scope, and an id outside it resolves to nothing. The Admin SDK
38
+ * does not evaluate rules, so AGL-1041 does not cover this path; the filter
39
+ * has to live here.
40
+ */
41
+ export declare function getDatasets(options: {
42
+ hostId: string;
43
+ keys: readonly string[];
44
+ }): Promise<Record<string, Aglyn.RepeatableDataset>>;
45
+ export default getDatasets;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import * as Aglyn from "@aglyn/aglyn/server";
17
+ import { orgDataQueryForHost } from "@aglyn/tenant-data-admin";
18
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
19
+ import { FieldPath } from "firebase-admin/firestore";
20
+ import { repeatRecordsFromPages } from "./repeat-record-pages.js";
21
+ /**
22
+ * The render path's largest read (AGL-1302): up to two pages of records per
23
+ * dataset a page repeats over. Records are mapped to plain values before
24
+ * return, so the cached shape is what repeatable expansion consumes. Backstop
25
+ * only; the publish path busts the tag — see PUBLISHED_SITE_DATA_TTL_SECONDS.
26
+ */ const DATASETS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
27
+ /**
28
+ * The datasets a page repeats over, with their records, for repeatable
29
+ * expansion (AGL-103).
30
+ *
31
+ * `keys` are the page's repeat keys (`repeatDatasetKeys`), each a dataset id or
32
+ * a display name — editors type the friendly name into the Repeat attribute.
33
+ * The map answers under every key exactly as written, and under every loaded
34
+ * dataset's id, which is how a reference hop (AGL-180) finds its target.
35
+ * Records are editor-ordered and capped at the repeat bound.
36
+ *
37
+ * Only the datasets the page names are read. A site may see thousands of
38
+ * datasets, and a read of "the site's datasets" can only ever be a bounded
39
+ * page of them, so a repeat bound to one outside that page rendered its
40
+ * template once with nothing to say why.
41
+ *
42
+ * Fail-open, per key: a key that cannot be read is left out, and its
43
+ * repeatable renders its template untouched, without costing the others.
44
+ *
45
+ * Scoped to what THIS host may see (AGL-1039): a display name resolves inside
46
+ * the host's scope, and an id outside it resolves to nothing. The Admin SDK
47
+ * does not evaluate rules, so AGL-1041 does not cover this path; the filter
48
+ * has to live here.
49
+ */ export async function getDatasets(options) {
50
+ const keys = [
51
+ ...new Set(options.keys.map((key)=>key.trim()).filter(Boolean))
52
+ ].sort();
53
+ if (!keys.length) return {};
54
+ try {
55
+ return await withRenderCache({
56
+ key: [
57
+ 'tenant-datasets',
58
+ options.hostId,
59
+ ...keys
60
+ ],
61
+ revalidate: DATASETS_TTL_SECONDS,
62
+ tags: [
63
+ tenantDataTag(options.hostId)
64
+ ],
65
+ read: ()=>readDatasets(options.hostId, keys)
66
+ });
67
+ } catch (error) {
68
+ console.error(error);
69
+ return readDatasets(options.hostId, keys);
70
+ }
71
+ }
72
+ async function readDatasets(hostId, keys) {
73
+ const datasets = {};
74
+ try {
75
+ var _ref_parent_parent, _ref_parent;
76
+ const { ref, query } = await orgDataQueryForHost(hostId, 'datasets');
77
+ // The same visibility rule `resolveDatasetDoc` applies: a keyed read
78
+ // bypasses the scoped query, so the scope is checked on the document.
79
+ const orgScoped = ((_ref_parent = ref.parent) == null ? void 0 : (_ref_parent_parent = _ref_parent.parent) == null ? void 0 : _ref_parent_parent.id) === 'orgs';
80
+ const usable = (snapshot)=>Boolean(snapshot == null ? void 0 : snapshot.exists) && !(snapshot == null ? void 0 : snapshot.get('deletedAt')) && (!orgScoped || Aglyn.visibleToHost(snapshot == null ? void 0 : snapshot.get('visibleTo'), hostId));
81
+ const loads = new Map();
82
+ const load = (snapshot)=>{
83
+ let dataset = loads.get(snapshot.id);
84
+ if (!dataset) {
85
+ dataset = readRepeatRecords(snapshot.ref).then((records)=>({
86
+ records,
87
+ model: Aglyn.effectiveDatasetModel(snapshot.data())
88
+ }));
89
+ loads.set(snapshot.id, dataset);
90
+ }
91
+ return dataset;
92
+ };
93
+ // An id first, then a display name. An id that exists but that this host
94
+ // cannot see does not fall through to the name: that would answer "which
95
+ // dataset is called X" for a key that already named a specific one.
96
+ const resolveKey = async (key)=>{
97
+ if (!key.includes('/')) {
98
+ const byId = await ref.doc(key).get();
99
+ if (byId.exists) return usable(byId) ? byId : undefined;
100
+ }
101
+ const byName = await query.where('displayName', '==', key).limit(1).get();
102
+ return byName.docs.find((snapshot)=>usable(snapshot));
103
+ };
104
+ const resolved = await Promise.all(keys.map(async (key)=>{
105
+ try {
106
+ const snapshot = await resolveKey(key);
107
+ if (!snapshot) return undefined;
108
+ const dataset = await load(snapshot);
109
+ datasets[key] = dataset;
110
+ datasets[snapshot.id] = dataset;
111
+ return dataset;
112
+ } catch (error) {
113
+ console.error(error);
114
+ return undefined;
115
+ }
116
+ }));
117
+ // One reference hop (AGL-180): `{{item.author.name}}` reads the target's
118
+ // rows by id, so the target has to be loaded even though no repeat names it.
119
+ const targets = new Set();
120
+ for (const dataset of resolved){
121
+ var _ref;
122
+ var _dataset_model;
123
+ for (const fieldId of (_ref = dataset == null ? void 0 : (_dataset_model = dataset.model) == null ? void 0 : _dataset_model.order) != null ? _ref : []){
124
+ var _dataset_model1, _field_reference;
125
+ const field = dataset == null ? void 0 : (_dataset_model1 = dataset.model) == null ? void 0 : _dataset_model1.fields[fieldId];
126
+ const targetId = (field == null ? void 0 : field.type) === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId : undefined;
127
+ if (targetId && !targetId.includes('/') && !datasets[targetId]) {
128
+ targets.add(targetId);
129
+ }
130
+ }
131
+ }
132
+ await Promise.all([
133
+ ...targets
134
+ ].map(async (targetId)=>{
135
+ try {
136
+ const target = await ref.doc(targetId).get();
137
+ if (usable(target)) datasets[targetId] = await load(target);
138
+ } catch (error) {
139
+ console.error(error);
140
+ }
141
+ }));
142
+ } catch (error) {
143
+ console.error(error);
144
+ }
145
+ return datasets;
146
+ }
147
+ /**
148
+ * The records a repeat renders, in the order it renders them.
149
+ *
150
+ * `orderBy('order')` reads the first group, and matches only documents that
151
+ * carry the field. When it comes back short, every ordered record is already
152
+ * in hand — so at most that many rows of the first page by document id can be
153
+ * ordered, and the rest of that page is exactly the first unordered records.
154
+ * Two bounded reads, never a walk of the collection; which rows win, and in
155
+ * what order, is {@link repeatRecordsFromPages}, the rule the besigner's
156
+ * canvas preview reads through as well (AGL-3111).
157
+ */ async function readRepeatRecords(datasetRef) {
158
+ const recordsRef = datasetRef.collection('records');
159
+ const page = (snapshot)=>snapshot.docs.map((doc)=>({
160
+ id: doc.id,
161
+ data: doc.data()
162
+ }));
163
+ const byOrder = await recordsRef.orderBy('order').limit(Aglyn.REPEAT_MAX_RECORDS).get();
164
+ const byId = byOrder.docs.length < Aglyn.REPEAT_MAX_RECORDS ? await recordsRef.orderBy(FieldPath.documentId()).limit(Aglyn.REPEAT_MAX_RECORDS).get() : undefined;
165
+ return repeatRecordsFromPages(page(byOrder), byId ? page(byId) : []);
166
+ }
167
+ export default getDatasets;
168
+
169
+ //# sourceMappingURL=get-datasets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-datasets.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { orgDataQueryForHost } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport { FieldPath } from 'firebase-admin/firestore'\nimport { repeatRecordsFromPages } from './repeat-record-pages'\n\n/**\n * The render path's largest read (AGL-1302): up to two pages of records per\n * dataset a page repeats over. Records are mapped to plain values before\n * return, so the cached shape is what repeatable expansion consumes. Backstop\n * only; the publish path busts the tag — see PUBLISHED_SITE_DATA_TTL_SECONDS.\n */\nconst DATASETS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * The datasets a page repeats over, with their records, for repeatable\n * expansion (AGL-103).\n *\n * `keys` are the page's repeat keys (`repeatDatasetKeys`), each a dataset id or\n * a display name — editors type the friendly name into the Repeat attribute.\n * The map answers under every key exactly as written, and under every loaded\n * dataset's id, which is how a reference hop (AGL-180) finds its target.\n * Records are editor-ordered and capped at the repeat bound.\n *\n * Only the datasets the page names are read. A site may see thousands of\n * datasets, and a read of \"the site's datasets\" can only ever be a bounded\n * page of them, so a repeat bound to one outside that page rendered its\n * template once with nothing to say why.\n *\n * Fail-open, per key: a key that cannot be read is left out, and its\n * repeatable renders its template untouched, without costing the others.\n *\n * Scoped to what THIS host may see (AGL-1039): a display name resolves inside\n * the host's scope, and an id outside it resolves to nothing. The Admin SDK\n * does not evaluate rules, so AGL-1041 does not cover this path; the filter\n * has to live here.\n */\nexport async function getDatasets(options: {\n hostId: string\n keys: readonly string[]\n}): Promise<Record<string, Aglyn.RepeatableDataset>> {\n const keys = [\n ...new Set(options.keys.map((key) => key.trim()).filter(Boolean)),\n ].sort()\n if (!keys.length) return {}\n try {\n return await withRenderCache({\n key: ['tenant-datasets', options.hostId, ...keys],\n revalidate: DATASETS_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readDatasets(options.hostId, keys),\n })\n } catch (error) {\n console.error(error)\n return readDatasets(options.hostId, keys)\n }\n}\n\nasync function readDatasets(\n hostId: string,\n keys: readonly string[],\n): Promise<Record<string, Aglyn.RepeatableDataset>> {\n const datasets: Record<string, Aglyn.RepeatableDataset> = {}\n try {\n const { ref, query } = await orgDataQueryForHost(hostId, 'datasets')\n // The same visibility rule `resolveDatasetDoc` applies: a keyed read\n // bypasses the scoped query, so the scope is checked on the document.\n const orgScoped = ref.parent?.parent?.id === 'orgs'\n const usable = (snapshot: FirebaseFirestore.DocumentSnapshot | undefined) =>\n Boolean(snapshot?.exists) &&\n !snapshot?.get('deletedAt') &&\n (!orgScoped || Aglyn.visibleToHost(snapshot?.get('visibleTo'), hostId))\n\n const loads = new Map<string, Promise<Aglyn.RepeatableDataset>>()\n const load = (snapshot: FirebaseFirestore.DocumentSnapshot) => {\n let dataset = loads.get(snapshot.id)\n if (!dataset) {\n dataset = readRepeatRecords(snapshot.ref).then((records) => ({\n records,\n model: Aglyn.effectiveDatasetModel(\n snapshot.data() as Aglyn.HostDataset,\n ),\n }))\n loads.set(snapshot.id, dataset)\n }\n return dataset\n }\n\n // An id first, then a display name. An id that exists but that this host\n // cannot see does not fall through to the name: that would answer \"which\n // dataset is called X\" for a key that already named a specific one.\n const resolveKey = async (key: string) => {\n if (!key.includes('/')) {\n const byId = await ref.doc(key).get()\n if (byId.exists) return usable(byId) ? byId : undefined\n }\n const byName = await query.where('displayName', '==', key).limit(1).get()\n return byName.docs.find((snapshot) => usable(snapshot))\n }\n\n const resolved = await Promise.all(\n keys.map(async (key) => {\n try {\n const snapshot = await resolveKey(key)\n if (!snapshot) return undefined\n const dataset = await load(snapshot)\n datasets[key] = dataset\n datasets[snapshot.id] = dataset\n return dataset\n } catch (error) {\n console.error(error)\n return undefined\n }\n }),\n )\n\n // One reference hop (AGL-180): `{{item.author.name}}` reads the target's\n // rows by id, so the target has to be loaded even though no repeat names it.\n const targets = new Set<string>()\n for (const dataset of resolved) {\n for (const fieldId of dataset?.model?.order ?? []) {\n const field = dataset?.model?.fields[fieldId]\n const targetId =\n field?.type === 'reference' ? field.reference?.datasetId : undefined\n if (targetId && !targetId.includes('/') && !datasets[targetId]) {\n targets.add(targetId)\n }\n }\n }\n await Promise.all(\n [...targets].map(async (targetId) => {\n try {\n const target = await ref.doc(targetId).get()\n if (usable(target)) datasets[targetId] = await load(target)\n } catch (error) {\n console.error(error)\n }\n }),\n )\n } catch (error) {\n console.error(error)\n }\n return datasets\n}\n\n/**\n * The records a repeat renders, in the order it renders them.\n *\n * `orderBy('order')` reads the first group, and matches only documents that\n * carry the field. When it comes back short, every ordered record is already\n * in hand — so at most that many rows of the first page by document id can be\n * ordered, and the rest of that page is exactly the first unordered records.\n * Two bounded reads, never a walk of the collection; which rows win, and in\n * what order, is {@link repeatRecordsFromPages}, the rule the besigner's\n * canvas preview reads through as well (AGL-3111).\n */\nasync function readRepeatRecords(\n datasetRef: FirebaseFirestore.DocumentReference,\n): Promise<Array<Record<string, unknown>>> {\n const recordsRef = datasetRef.collection('records')\n const page = (snapshot: FirebaseFirestore.QuerySnapshot) =>\n snapshot.docs.map((doc) => ({\n id: doc.id,\n data: doc.data() as Aglyn.HostDatasetRecord,\n }))\n const byOrder = await recordsRef\n .orderBy('order')\n .limit(Aglyn.REPEAT_MAX_RECORDS)\n .get()\n const byId =\n byOrder.docs.length < Aglyn.REPEAT_MAX_RECORDS\n ? await recordsRef\n .orderBy(FieldPath.documentId())\n .limit(Aglyn.REPEAT_MAX_RECORDS)\n .get()\n : undefined\n return repeatRecordsFromPages(page(byOrder), byId ? page(byId) : [])\n}\n\nexport default getDatasets\n"],"names":["Aglyn","orgDataQueryForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","FieldPath","repeatRecordsFromPages","DATASETS_TTL_SECONDS","getDatasets","options","keys","Set","map","key","trim","filter","Boolean","sort","length","hostId","revalidate","tags","read","readDatasets","error","console","datasets","ref","query","orgScoped","parent","id","usable","snapshot","exists","get","visibleToHost","loads","Map","load","dataset","readRepeatRecords","then","records","model","effectiveDatasetModel","data","set","resolveKey","includes","byId","doc","undefined","byName","where","limit","docs","find","resolved","Promise","all","targets","fieldId","order","field","fields","targetId","type","reference","datasetId","add","target","datasetRef","recordsRef","collection","page","byOrder","orderBy","REPEAT_MAX_RECORDS","documentId"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as Aglyn from '@aglyn/aglyn/server';
18
+ export declare function getForms(options: {
19
+ hostId: Aglyn.HostUid;
20
+ }): Promise<{
21
+ forms: Record<string, Aglyn.PlacedFormDesign>;
22
+ error: unknown;
23
+ }>;
24
+ export default getForms;