@aglyn/tenant-data-admin 1.0.0-beta.188 → 1.0.0-beta.190

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-data-admin",
3
- "version": "1.0.0-beta.188",
3
+ "version": "1.0.0-beta.190",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,12 +25,12 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.188",
29
- "@aglyn/shared-util-email": "1.0.0-beta.188",
30
- "@aglyn/shared-util-fbserver": "1.0.0-beta.188",
31
- "@aglyn/shared-util-first-touch": "1.0.0-beta.188",
32
- "@aglyn/shared-util-http": "1.0.0-beta.188",
33
- "@aglyn/shared-util-tools": "1.0.0-beta.188",
28
+ "@aglyn/aglyn": "1.0.0-beta.190",
29
+ "@aglyn/shared-util-email": "1.0.0-beta.190",
30
+ "@aglyn/shared-util-fbserver": "1.0.0-beta.190",
31
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.190",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.190",
33
+ "@aglyn/shared-util-tools": "1.0.0-beta.190",
34
34
  "@msgpack/msgpack": "^3.1.3",
35
35
  "@swc/helpers": "0.5.23",
36
36
  "sharp": "^0.35.3",
@@ -75,8 +75,14 @@ export interface UsageDependent {
75
75
  * `screen` because it is never a page. A caller that took it for one sent
76
76
  * the reader to the page settings — a slug, SEO and a Publish button that
77
77
  * would serve the email as a route — and a cache drop has no page to drop.
78
+ *
79
+ * `systemEmail` is one of the platform's own emails
80
+ * (`systemEmailTemplates/{key}`, at the root, belonging to no site), `id`
81
+ * its catalog key (AGL-3318). Only the platform marketing site's blocks
82
+ * have one: that is the site those emails read their header and footer
83
+ * from.
78
84
  */
79
- type: 'screen' | 'layout' | 'component' | 'collection' | 'emailTemplate' | 'emailDesign';
85
+ type: 'screen' | 'layout' | 'component' | 'collection' | 'emailTemplate' | 'emailDesign' | 'systemEmail';
80
86
  id: string;
81
87
  name: string;
82
88
  via: Array<'id' | 'name'>;
@@ -107,6 +113,13 @@ export interface UsageSources {
107
113
  * has any use for them, and none reads them.
108
114
  */
109
115
  emailTemplates?: UsageCandidate[];
116
+ /**
117
+ * The platform's own emails, each on its published version (AGL-3318).
118
+ * Read only when the site scanned is the platform marketing site, whose
119
+ * email blocks those emails place, and listed by {@link scanComponentUsage}
120
+ * alone for the reason `emailTemplates` is: an email is not a page.
121
+ */
122
+ systemEmails?: UsageCandidate[];
110
123
  }
111
124
  /** `displayName`, falling back to a legacy `name`, then the raw id. */
112
125
  export declare function usageCandidateLabel(candidate: UsageCandidate): string;
@@ -120,9 +133,11 @@ export declare const isLiveUsageCandidate: (candidate: UsageCandidate) => boolea
120
133
  * versions, OTHER component definitions — `composeReusableComponentNodes`
121
134
  * grafts nested instances, so a component used only inside another component
122
135
  * is genuinely used — and, when the caller read them, the site's transactional
123
- * emails, which graft a placed header or footer at send time (AGL-3287).
124
- * Omitting any of them would report "used nowhere" for something that renders
125
- * and invite a confident deletion, which is worse than showing nothing at all.
136
+ * emails, which graft a placed header or footer at send time (AGL-3287), and
137
+ * the platform's own emails, which graft the platform marketing site's header
138
+ * and footer (AGL-3318). Omitting any of them would report "used nowhere" for
139
+ * something that renders and invite a confident deletion, which is worse than
140
+ * showing nothing at all.
126
141
  */
127
142
  export declare function scanComponentUsage(componentId: string, sources: UsageSources): UsageDependent[];
128
143
  /**
@@ -220,6 +235,21 @@ export declare function readUsageCandidates(hostRef: FirebaseFirestore.DocumentR
220
235
  withNodes: boolean;
221
236
  limit: number;
222
237
  }): Promise<UsageCandidateRead>;
238
+ /**
239
+ * The platform's own emails as a usage corpus (AGL-3318).
240
+ *
241
+ * They live at the ROOT, `systemEmailTemplates/{key}`, because they belong to
242
+ * no site, with the shape a site's own emails have: the published `versionId`
243
+ * on the template, the tree on that version, and no name but the catalog's.
244
+ * Only a scan of the platform marketing site asks for them, since its email
245
+ * blocks are the ones they place. Read under the same bound, and reporting
246
+ * the same `truncated`, as a site's collections; the catalog is code, so a
247
+ * document exists only for an email somebody designed, and the read is a
248
+ * handful of documents.
249
+ */
250
+ export declare function readSystemEmailUsageCandidates(firestore: Pick<FirebaseFirestore.Firestore, 'collection'>, options: {
251
+ limit: number;
252
+ }): Promise<UsageCandidateRead>;
223
253
  /**
224
254
  * Every screen, layout and component of a site, with their node trees — the
225
255
  * corpus every tree-searching closure above walks.
@@ -31,6 +31,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
31
31
  * console's `scan-artifact-usage` re-exports these and keeps the parts that
32
32
  * are about the console's own "what would I break" copy.
33
33
  */ import { nodesReferenceComponent, decodeStoredNodes } from "@aglyn/aglyn/server";
34
+ import { getSystemEmailTemplate, SYSTEM_EMAIL_COLLECTION } from "@aglyn/shared-util-email/system-email-catalog";
34
35
  import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-email/tenant-email-catalog";
35
36
  /** `displayName`, falling back to a legacy `name`, then the raw id. */ export function usageCandidateLabel(candidate) {
36
37
  var _ref, _candidate_displayName;
@@ -45,11 +46,13 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
45
46
  * versions, OTHER component definitions — `composeReusableComponentNodes`
46
47
  * grafts nested instances, so a component used only inside another component
47
48
  * is genuinely used — and, when the caller read them, the site's transactional
48
- * emails, which graft a placed header or footer at send time (AGL-3287).
49
- * Omitting any of them would report "used nowhere" for something that renders
50
- * and invite a confident deletion, which is worse than showing nothing at all.
49
+ * emails, which graft a placed header or footer at send time (AGL-3287), and
50
+ * the platform's own emails, which graft the platform marketing site's header
51
+ * and footer (AGL-3318). Omitting any of them would report "used nowhere" for
52
+ * something that renders and invite a confident deletion, which is worse than
53
+ * showing nothing at all.
51
54
  */ export function scanComponentUsage(componentId, sources) {
52
- var _sources_emailTemplates;
55
+ var _sources_emailTemplates, _sources_systemEmails;
53
56
  if (!componentId) return [];
54
57
  const dependents = [];
55
58
  const collect = (candidates, type)=>{
@@ -75,6 +78,7 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
75
78
  collect(sources.layouts, 'layout');
76
79
  collect(sources.components, 'component');
77
80
  collect((_sources_emailTemplates = sources.emailTemplates) != null ? _sources_emailTemplates : [], 'emailTemplate');
81
+ collect((_sources_systemEmails = sources.systemEmails) != null ? _sources_systemEmails : [], 'systemEmail');
78
82
  return dependents;
79
83
  }
80
84
  /**
@@ -208,11 +212,12 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
208
212
  seenComponents.add(dependent.id);
209
213
  next.push(dependent.id);
210
214
  }
211
- // An email — transactional or designed for a campaign — is not a page
212
- // and nothing caches it, so it contributes no screen and is not
213
- // followed (AGL-3287). A transactional email's id is a catalog key,
214
- // and walking it as a component would read a key as a definition that
215
- // does not exist.
215
+ // An email — transactional, designed for a campaign, or one of the
216
+ // platform's own — is not a page and nothing caches it, so it
217
+ // contributes no screen and is not followed (AGL-3287, AGL-3318). A
218
+ // transactional or platform email's id is a catalog key, and walking
219
+ // it as a component would read a key as a definition that does not
220
+ // exist.
216
221
  }
217
222
  }
218
223
  frontier = next;
@@ -236,15 +241,35 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
236
241
  * costs one empty query. Its documents carry no name, so each is labelled with
237
242
  * the catalog's.
238
243
  */ export async function readUsageCandidates(hostRef, collectionName, options) {
244
+ return readCandidatesIn(hostRef.collection(collectionName), collectionName, options);
245
+ }
246
+ /**
247
+ * The platform's own emails as a usage corpus (AGL-3318).
248
+ *
249
+ * They live at the ROOT, `systemEmailTemplates/{key}`, because they belong to
250
+ * no site, with the shape a site's own emails have: the published `versionId`
251
+ * on the template, the tree on that version, and no name but the catalog's.
252
+ * Only a scan of the platform marketing site asks for them, since its email
253
+ * blocks are the ones they place. Read under the same bound, and reporting
254
+ * the same `truncated`, as a site's collections; the catalog is code, so a
255
+ * document exists only for an email somebody designed, and the read is a
256
+ * handful of documents.
257
+ */ export async function readSystemEmailUsageCandidates(firestore, options) {
258
+ return readCandidatesIn(firestore.collection(SYSTEM_EMAIL_COLLECTION), SYSTEM_EMAIL_COLLECTION, {
259
+ withNodes: true,
260
+ limit: options.limit
261
+ });
262
+ }
263
+ /** The read both of the above make, over the collection each one names. */ async function readCandidatesIn(collection, collectionName, options) {
239
264
  const { withNodes, limit } = options;
240
265
  // One over the limit: if the extra document comes back, there was more than
241
266
  // we are about to look at. Cheaper than a count() and exact.
242
- const docs = await hostRef.collection(collectionName).limit(limit + 1).get();
267
+ const docs = await collection.limit(limit + 1).get();
243
268
  const truncated = docs.size > limit;
244
269
  const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs;
245
270
  const candidates = await Promise.all(inScope.map(async (docSnapshot)=>{
246
- var _ref;
247
- var _getTenantEmail;
271
+ var _ref, _ref1;
272
+ var _getTenantEmail, _getSystemEmailTemplate;
248
273
  const versionId = docSnapshot.get('versionId');
249
274
  // Components keep their tree on the document; screens and layouts keep
250
275
  // it on the published version.
@@ -264,7 +289,7 @@ import { getTenantEmail, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-emai
264
289
  const nodes = collectionName === 'components' ? decodeStoredNodes(docSnapshot.get('nodes')) : withNodes && versionId ? await docSnapshot.ref.collection('versions').doc(String(versionId)).get().then((version)=>decodeStoredNodes(version.get('nodes'))).catch(()=>null) : null;
265
290
  return _extends({
266
291
  id: docSnapshot.id,
267
- displayName: collectionName === TENANT_EMAIL_COLLECTION ? (_ref = (_getTenantEmail = getTenantEmail(docSnapshot.id)) == null ? void 0 : _getTenantEmail.name) != null ? _ref : docSnapshot.id : docSnapshot.get('displayName'),
292
+ displayName: collectionName === TENANT_EMAIL_COLLECTION ? (_ref = (_getTenantEmail = getTenantEmail(docSnapshot.id)) == null ? void 0 : _getTenantEmail.name) != null ? _ref : docSnapshot.id : collectionName === SYSTEM_EMAIL_COLLECTION ? (_ref1 = (_getSystemEmailTemplate = getSystemEmailTemplate(docSnapshot.id)) == null ? void 0 : _getSystemEmailTemplate.name) != null ? _ref1 : docSnapshot.id : docSnapshot.get('displayName'),
268
293
  name: docSnapshot.get('name'),
269
294
  deletedAt: docSnapshot.get('deletedAt'),
270
295
  nodes
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/live-page-usage.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\n/**\n * Which live pages a change reaches, for a site's screens, layouts and\n * components.\n *\n * The corpus read and the two closures over it, held here rather than in the\n * console because the console is no longer the only side that needs them. A\n * dataset record is written from the tenant as well — a form submission, an\n * automation step — and those writes make the same pages stale as a console\n * edit does. Two implementations of \"which screens render this\" is the shape\n * that put AGL-1223 in the tree: one reader handled a node tree's second\n * storage form and the other did not, so half the corpus answered \"used\n * nowhere\" and a publish dropped no cache.\n *\n * Nothing here is dataset-specific, and nothing is console-specific. The\n * console's `scan-artifact-usage` re-exports these and keeps the parts that\n * are about the console's own \"what would I break\" copy.\n */\n\nimport {\n nodesReferenceComponent,\n decodeStoredNodes,\n type ReusableComponentProp,\n} from '@aglyn/aglyn/server'\nimport {\n getTenantEmail,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email/tenant-email-catalog'\n\n/** A screen/layout/component reduced to what a usage scan needs. */\nexport interface UsageCandidate {\n id: string\n displayName?: string\n /** Legacy field some older documents used instead of `displayName`. */\n name?: string\n deletedAt?: unknown\n /**\n * Node tree to search. For screens and layouts this is the PUBLISHED\n * version's nodes (what visitors see); for components it is the definition\n * tree off the component document, which is what the runtime reads.\n */\n nodes?: Record<string, any> | null\n /** Published version, carried through so the caller can deep-link. */\n versionId?: string\n /** Screens only: the layout they render inside. */\n layoutId?: string\n /** Screens only: the screen they nest under, which is part of their path. */\n parentId?: string\n /**\n * Screens only: `'email'` for a campaign email design (AGL-3287), which\n * a scan reports as an `emailDesign` rather than a page.\n */\n kind?: string\n /**\n * Components only: the properties the definition declares (AGL-1247). A\n * Link property's default renders as a link wherever an instance leaves the\n * property unset, and it is stored here, not in `nodes` (AGL-2846).\n */\n props?: ReadonlyArray<ReusableComponentProp | null | undefined> | null\n}\n\n/** One document that depends on the artifact a scan was asked about. */\nexport interface UsageDependent {\n /**\n * `emailTemplate` is one of the site's own transactional emails\n * (`hosts/{h}/emailTemplates/{key}`), `id` its catalog key (AGL-3287).\n *\n * `emailDesign` is an email designed for a campaign: a `kind: 'email'`\n * screen document, `id` its screen id. It is its own type rather than a\n * `screen` because it is never a page. A caller that took it for one sent\n * the reader to the page settings — a slug, SEO and a Publish button that\n * would serve the email as a route — and a cache drop has no page to drop.\n */\n type:\n | 'screen'\n | 'layout'\n | 'component'\n | 'collection'\n | 'emailTemplate'\n | 'emailDesign'\n id: string\n name: string\n via: Array<'id' | 'name'>\n versionId?: string\n /**\n * HOW the dependent references the artifact — screens and collection\n * listings (AGL-703, AGL-2806).\n *\n * A component or a layout has exactly one kind of dependent and the noun\n * says everything: an instance, or a binding. A screen has three, and they\n * break in three different ways — a link goes dead, a child moves, a\n * collection loses the page it renders through. Copy that could not tell\n * them apart would have to describe the worst case every time. A collection\n * listing's dependents are all links, and say so.\n */\n relation?: 'link' | 'child' | 'template'\n}\n\n/** The three corpora every closure below walks, and a fourth for components. */\nexport interface UsageSources {\n screens: UsageCandidate[]\n layouts: UsageCandidate[]\n components: UsageCandidate[]\n /**\n * The site's transactional email designs, each on its published version\n * (AGL-3287). Read only where a scan asks \"what places this component\" —\n * {@link scanComponentUsage} lists them — because an email is not a page:\n * nothing is cached for it, so no closure that decides which pages to drop\n * has any use for them, and none reads them.\n */\n emailTemplates?: UsageCandidate[]\n}\n\n/** `displayName`, falling back to a legacy `name`, then the raw id. */\nexport function usageCandidateLabel(candidate: UsageCandidate): string {\n return String(candidate.displayName ?? candidate.name ?? candidate.id)\n}\n\n/** A soft-deleted document renders nothing, so it depends on nothing. */\nexport const isLiveUsageCandidate = (candidate: UsageCandidate): boolean =>\n !candidate.deletedAt\n\n/**\n * Everything that references a reusable component (AGL-703).\n *\n * Every place the renderer expands an instance: published screen versions\n * (email designs among them — a campaign email is a screen), published layout\n * versions, OTHER component definitions — `composeReusableComponentNodes`\n * grafts nested instances, so a component used only inside another component\n * is genuinely used — and, when the caller read them, the site's transactional\n * emails, which graft a placed header or footer at send time (AGL-3287).\n * Omitting any of them would report \"used nowhere\" for something that renders\n * and invite a confident deletion, which is worse than showing nothing at all.\n */\nexport function scanComponentUsage(\n componentId: string,\n sources: UsageSources,\n): UsageDependent[] {\n if (!componentId) return []\n const dependents: UsageDependent[] = []\n const collect = (\n candidates: UsageCandidate[],\n type: UsageDependent['type'],\n ) => {\n for (const candidate of candidates) {\n if (!isLiveUsageCandidate(candidate)) continue\n // A component never counts as using itself, however it nests.\n if (type === 'component' && candidate.id === componentId) continue\n if (!nodesReferenceComponent(candidate.nodes, componentId)) continue\n dependents.push({\n type:\n type === 'screen' && candidate.kind === 'email'\n ? 'emailDesign'\n : type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n // Instances reference by id, so a rename can never break them.\n via: ['id'],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n })\n }\n }\n collect(sources.screens, 'screen')\n collect(sources.layouts, 'layout')\n collect(sources.components, 'component')\n collect(sources.emailTemplates ?? [], 'emailTemplate')\n return dependents\n}\n\n/**\n * Everything rendering inside a layout (AGL-703).\n *\n * Two kinds of dependent, both expressed by the same `layoutId` pointer:\n *\n * - **screens**, which name the layout they render inside;\n * - **other layouts**, since a layout can itself sit inside one. A nested\n * layout is a real dependent — deleting its parent unwraps every screen\n * underneath it — so leaving layouts out would report a parent layout as\n * used only by the screens that name it directly, and none of the ones\n * that reach it through a child.\n *\n * A layout never counts as its own dependent; `canNestLayout` refuses that,\n * and this refuses to report it even if stored data holds one.\n */\nexport function scanLayoutUsage(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): UsageDependent[] {\n if (!layoutId) return []\n const dependentsOf = (\n candidates: UsageCandidate[],\n type: 'screen' | 'layout',\n ) =>\n candidates\n .filter(\n (candidate) =>\n isLiveUsageCandidate(candidate) &&\n candidate.layoutId === layoutId &&\n candidate.id !== layoutId,\n )\n .map((candidate) => ({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n via: ['id' as const],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n }))\n return [\n ...dependentsOf(screens, 'screen'),\n ...dependentsOf(layouts, 'layout'),\n ]\n}\n\n/**\n * Every live screen rendered inside `layoutId`, at ANY nesting depth\n * (AGL-1150).\n *\n * `scanLayoutUsage` answers one level. Layouts nest — a screen points at a\n * layout, which can point at a parent layout, and `compose-screen-nodes` walks\n * that whole chain when composing a page. So publishing a layout changes every\n * screen below it, not just the ones bound to it directly, and a cache drop\n * that only handles the direct level leaves the rest showing stale chrome for\n * the full revalidate window.\n *\n * Pure, and separate from the Firestore read, so the nesting behaviour is\n * testable without a database.\n *\n * Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written\n * straight to Firestore is not bound by that, and a cycle here would hang a\n * publish request rather than surface anything.\n */\nexport function screenIdsUsingLayoutDeep(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): string[] {\n if (!layoutId) return []\n const screenIds = new Set<string>()\n const seenLayouts = new Set<string>([layoutId])\n let frontier = [layoutId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanLayoutUsage(id, screens, layouts)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (!seenLayouts.has(dependent.id)) {\n seenLayouts.add(dependent.id)\n next.push(dependent.id)\n }\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\n/**\n * Every live screen whose rendered output contains `componentId`, however\n * indirectly (AGL-1161).\n *\n * `scanComponentUsage` answers one level and returns three kinds of dependent.\n * Only one of them is a screen, and the other two both reach screens by routes\n * a single-level scan cannot see:\n *\n * - a **component** dependent nests the target inside itself, and that outer\n * component may itself only be used inside a third — so component→component\n * edges have to be followed to a fixed point;\n * - a **layout** dependent puts the component in page chrome, which every\n * screen under that layout renders. Layouts nest, so that is\n * `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.\n *\n * Miss either and a publish reports success while some pages keep serving the\n * old component for the full revalidate window — the failure this whole arc\n * exists to remove, and the one that is hardest to notice because the pages\n * that ARE dropped update instantly.\n *\n * Pure, and separate from the Firestore read, so the closure is testable\n * without a database — the same split `screenIdsUsingLayoutDeep` uses.\n *\n * Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a\n * document written straight to Firestore is not bound by what the editor\n * allows, and a cycle here would hang a publish rather than surface anything.\n */\nexport function screenIdsUsingComponentDeep(\n componentId: string,\n sources: UsageSources,\n): string[] {\n if (!componentId) return []\n const screenIds = new Set<string>()\n const seenComponents = new Set<string>([componentId])\n // Layouts are resolved through their own deep walk, so remember which ones\n // have already been expanded: two components in the same layout would\n // otherwise re-walk the whole layout tree once each.\n const seenLayouts = new Set<string>()\n let frontier = [componentId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanComponentUsage(id, sources)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (dependent.type === 'layout') {\n if (seenLayouts.has(dependent.id)) continue\n seenLayouts.add(dependent.id)\n // The layout itself renders no URL; the screens beneath it do.\n for (const screenId of screenIdsUsingLayoutDeep(\n dependent.id,\n sources.screens,\n sources.layouts,\n )) {\n screenIds.add(screenId)\n }\n } else if (\n dependent.type === 'component' &&\n !seenComponents.has(dependent.id)\n ) {\n seenComponents.add(dependent.id)\n next.push(dependent.id)\n }\n // An email — transactional or designed for a campaign — is not a page\n // and nothing caches it, so it contributes no screen and is not\n // followed (AGL-3287). A transactional email's id is a catalog key,\n // and walking it as a component would read a key as a definition that\n // does not exist.\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\nexport interface UsageCandidateRead {\n candidates: UsageCandidate[]\n /**\n * The collection held more documents than `limit` allowed, so the scan below\n * it is INCOMPLETE.\n *\n * Returned rather than logged, because the two callers owe the user\n * different things: an advisory \"what would I break\" can show a partial\n * answer and say so, while a cache drop that silently scans a prefix reports\n * a successful publish and leaves real pages stale.\n */\n truncated: boolean\n}\n\n/**\n * One collection's documents, with published nodes attached when the scan\n * needs to search them.\n *\n * `limit` is a real bound, not a guess: it is fetched with one extra document\n * so exceeding it is DETECTED rather than assumed away. A caller that ignores\n * `truncated` is choosing to be wrong quietly.\n *\n * `emailTemplates` — the site's transactional email designs (AGL-3287) — has\n * the screens' shape: a parent holding the published `versionId`, the tree on\n * that version. The catalog is code and fixed, and a template document only\n * exists once somebody pressed Design, so most sites have none and the read\n * costs one empty query. Its documents carry no name, so each is labelled with\n * the catalog's.\n */\nexport async function readUsageCandidates(\n hostRef: FirebaseFirestore.DocumentReference,\n collectionName:\n | 'screens'\n | 'layouts'\n | 'components'\n | typeof TENANT_EMAIL_COLLECTION,\n options: { withNodes: boolean; limit: number },\n): Promise<UsageCandidateRead> {\n const { withNodes, limit } = options\n // One over the limit: if the extra document comes back, there was more than\n // we are about to look at. Cheaper than a count() and exact.\n const docs = await hostRef.collection(collectionName).limit(limit + 1).get()\n const truncated = docs.size > limit\n const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs\n\n const candidates = await Promise.all(\n inScope.map(async (docSnapshot) => {\n const versionId = docSnapshot.get('versionId')\n // Components keep their tree on the document; screens and layouts keep\n // it on the published version.\n //\n // BOTH reads are decoded (AGL-1223). A component document is msgpack\n // for anything promoted since components were compressed and a plain\n // map for everything older, and `decodeStoredNodes` returns a map\n // unchanged — so the branch below cannot be simplified back into a raw\n // read on either side.\n //\n // What rides on this: every consumer walks the value with\n // `Object.values`, which over a `Buffer` yields byte NUMBERS and\n // matches nothing. The answer that produces is \"used nowhere\" — for\n // `/api/hosts/where-used` an invitation to delete something a live page\n // renders, and for `/api/screens/revalidate` a publish that drops no\n // cache and leaves the old page served.\n const nodes =\n collectionName === 'components'\n ? decodeStoredNodes(docSnapshot.get('nodes'))\n : withNodes && versionId\n ? await docSnapshot.ref\n .collection('versions')\n .doc(String(versionId))\n .get()\n .then((version) => decodeStoredNodes(version.get('nodes')))\n .catch(() => null)\n : null\n return {\n id: docSnapshot.id,\n displayName:\n collectionName === TENANT_EMAIL_COLLECTION\n ? (getTenantEmail(docSnapshot.id)?.name ?? docSnapshot.id)\n : docSnapshot.get('displayName'),\n name: docSnapshot.get('name'),\n deletedAt: docSnapshot.get('deletedAt'),\n nodes,\n ...(versionId ? { versionId: String(versionId) } : {}),\n ...(docSnapshot.get('layoutId')\n ? { layoutId: String(docSnapshot.get('layoutId')) }\n : {}),\n // Screens only, and only the screen scan reads it: a child screen is\n // a dependent of its parent, because its PATH is built from it\n // (AGL-703).\n ...(docSnapshot.get('parentId')\n ? { parentId: String(docSnapshot.get('parentId')) }\n : {}),\n // Screens only: a campaign email design is a screen document that is\n // never a page (AGL-3287).\n ...(collectionName === 'screens' && docSnapshot.get('kind')\n ? { kind: String(docSnapshot.get('kind')) }\n : {}),\n // Components only: the declared properties, whose Link defaults render\n // as links wherever an instance leaves one unset (AGL-2846). The same\n // document the tree came from, so it costs no read.\n ...(collectionName === 'components' &&\n Array.isArray(docSnapshot.get('props'))\n ? { props: docSnapshot.get('props') }\n : {}),\n } satisfies UsageCandidate\n }),\n )\n\n return { candidates, truncated }\n}\n\n/**\n * Every screen, layout and component of a site, with their node trees — the\n * corpus every tree-searching closure above walks.\n *\n * Each collection ONCE, in memory: a query per level would multiply round\n * trips by the nesting depth of the graph. Shared so two scans cannot read\n * different corpora under different bounds, and a change to one cannot quietly\n * narrow the other.\n */\nexport async function readUsageSources(\n hostRef: FirebaseFirestore.DocumentReference,\n limit: number,\n): Promise<{ candidates: UsageSources; truncated: boolean }> {\n const [screens, layouts, components] = await Promise.all([\n readUsageCandidates(hostRef, 'screens', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'layouts', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'components', { withNodes: true, limit }),\n ])\n return {\n candidates: {\n screens: screens.candidates,\n layouts: layouts.candidates,\n components: components.candidates,\n },\n truncated: screens.truncated || layouts.truncated || components.truncated,\n }\n}\n"],"names":["nodesReferenceComponent","decodeStoredNodes","getTenantEmail","TENANT_EMAIL_COLLECTION","usageCandidateLabel","candidate","String","displayName","name","id","isLiveUsageCandidate","deletedAt","scanComponentUsage","componentId","sources","dependents","collect","candidates","type","nodes","push","kind","via","versionId","screens","layouts","components","emailTemplates","scanLayoutUsage","layoutId","dependentsOf","filter","map","screenIdsUsingLayoutDeep","screenIds","Set","seenLayouts","frontier","length","next","dependent","add","has","screenIdsUsingComponentDeep","seenComponents","screenId","readUsageCandidates","hostRef","collectionName","options","withNodes","limit","docs","collection","get","truncated","size","inScope","slice","Promise","all","docSnapshot","ref","doc","then","version","catch","parentId","Array","isArray","props","readUsageSources"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/live-page-usage.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\n/**\n * Which live pages a change reaches, for a site's screens, layouts and\n * components.\n *\n * The corpus read and the two closures over it, held here rather than in the\n * console because the console is no longer the only side that needs them. A\n * dataset record is written from the tenant as well — a form submission, an\n * automation step — and those writes make the same pages stale as a console\n * edit does. Two implementations of \"which screens render this\" is the shape\n * that put AGL-1223 in the tree: one reader handled a node tree's second\n * storage form and the other did not, so half the corpus answered \"used\n * nowhere\" and a publish dropped no cache.\n *\n * Nothing here is dataset-specific, and nothing is console-specific. The\n * console's `scan-artifact-usage` re-exports these and keeps the parts that\n * are about the console's own \"what would I break\" copy.\n */\n\nimport {\n nodesReferenceComponent,\n decodeStoredNodes,\n type ReusableComponentProp,\n} from '@aglyn/aglyn/server'\nimport {\n getSystemEmailTemplate,\n SYSTEM_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email/system-email-catalog'\nimport {\n getTenantEmail,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email/tenant-email-catalog'\n\n/** A screen/layout/component reduced to what a usage scan needs. */\nexport interface UsageCandidate {\n id: string\n displayName?: string\n /** Legacy field some older documents used instead of `displayName`. */\n name?: string\n deletedAt?: unknown\n /**\n * Node tree to search. For screens and layouts this is the PUBLISHED\n * version's nodes (what visitors see); for components it is the definition\n * tree off the component document, which is what the runtime reads.\n */\n nodes?: Record<string, any> | null\n /** Published version, carried through so the caller can deep-link. */\n versionId?: string\n /** Screens only: the layout they render inside. */\n layoutId?: string\n /** Screens only: the screen they nest under, which is part of their path. */\n parentId?: string\n /**\n * Screens only: `'email'` for a campaign email design (AGL-3287), which\n * a scan reports as an `emailDesign` rather than a page.\n */\n kind?: string\n /**\n * Components only: the properties the definition declares (AGL-1247). A\n * Link property's default renders as a link wherever an instance leaves the\n * property unset, and it is stored here, not in `nodes` (AGL-2846).\n */\n props?: ReadonlyArray<ReusableComponentProp | null | undefined> | null\n}\n\n/** One document that depends on the artifact a scan was asked about. */\nexport interface UsageDependent {\n /**\n * `emailTemplate` is one of the site's own transactional emails\n * (`hosts/{h}/emailTemplates/{key}`), `id` its catalog key (AGL-3287).\n *\n * `emailDesign` is an email designed for a campaign: a `kind: 'email'`\n * screen document, `id` its screen id. It is its own type rather than a\n * `screen` because it is never a page. A caller that took it for one sent\n * the reader to the page settings — a slug, SEO and a Publish button that\n * would serve the email as a route — and a cache drop has no page to drop.\n *\n * `systemEmail` is one of the platform's own emails\n * (`systemEmailTemplates/{key}`, at the root, belonging to no site), `id`\n * its catalog key (AGL-3318). Only the platform marketing site's blocks\n * have one: that is the site those emails read their header and footer\n * from.\n */\n type:\n | 'screen'\n | 'layout'\n | 'component'\n | 'collection'\n | 'emailTemplate'\n | 'emailDesign'\n | 'systemEmail'\n id: string\n name: string\n via: Array<'id' | 'name'>\n versionId?: string\n /**\n * HOW the dependent references the artifact — screens and collection\n * listings (AGL-703, AGL-2806).\n *\n * A component or a layout has exactly one kind of dependent and the noun\n * says everything: an instance, or a binding. A screen has three, and they\n * break in three different ways — a link goes dead, a child moves, a\n * collection loses the page it renders through. Copy that could not tell\n * them apart would have to describe the worst case every time. A collection\n * listing's dependents are all links, and say so.\n */\n relation?: 'link' | 'child' | 'template'\n}\n\n/** The three corpora every closure below walks, and a fourth for components. */\nexport interface UsageSources {\n screens: UsageCandidate[]\n layouts: UsageCandidate[]\n components: UsageCandidate[]\n /**\n * The site's transactional email designs, each on its published version\n * (AGL-3287). Read only where a scan asks \"what places this component\" —\n * {@link scanComponentUsage} lists them — because an email is not a page:\n * nothing is cached for it, so no closure that decides which pages to drop\n * has any use for them, and none reads them.\n */\n emailTemplates?: UsageCandidate[]\n /**\n * The platform's own emails, each on its published version (AGL-3318).\n * Read only when the site scanned is the platform marketing site, whose\n * email blocks those emails place, and listed by {@link scanComponentUsage}\n * alone for the reason `emailTemplates` is: an email is not a page.\n */\n systemEmails?: UsageCandidate[]\n}\n\n/** `displayName`, falling back to a legacy `name`, then the raw id. */\nexport function usageCandidateLabel(candidate: UsageCandidate): string {\n return String(candidate.displayName ?? candidate.name ?? candidate.id)\n}\n\n/** A soft-deleted document renders nothing, so it depends on nothing. */\nexport const isLiveUsageCandidate = (candidate: UsageCandidate): boolean =>\n !candidate.deletedAt\n\n/**\n * Everything that references a reusable component (AGL-703).\n *\n * Every place the renderer expands an instance: published screen versions\n * (email designs among them — a campaign email is a screen), published layout\n * versions, OTHER component definitions — `composeReusableComponentNodes`\n * grafts nested instances, so a component used only inside another component\n * is genuinely used — and, when the caller read them, the site's transactional\n * emails, which graft a placed header or footer at send time (AGL-3287), and\n * the platform's own emails, which graft the platform marketing site's header\n * and footer (AGL-3318). Omitting any of them would report \"used nowhere\" for\n * something that renders and invite a confident deletion, which is worse than\n * showing nothing at all.\n */\nexport function scanComponentUsage(\n componentId: string,\n sources: UsageSources,\n): UsageDependent[] {\n if (!componentId) return []\n const dependents: UsageDependent[] = []\n const collect = (\n candidates: UsageCandidate[],\n type: UsageDependent['type'],\n ) => {\n for (const candidate of candidates) {\n if (!isLiveUsageCandidate(candidate)) continue\n // A component never counts as using itself, however it nests.\n if (type === 'component' && candidate.id === componentId) continue\n if (!nodesReferenceComponent(candidate.nodes, componentId)) continue\n dependents.push({\n type:\n type === 'screen' && candidate.kind === 'email'\n ? 'emailDesign'\n : type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n // Instances reference by id, so a rename can never break them.\n via: ['id'],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n })\n }\n }\n collect(sources.screens, 'screen')\n collect(sources.layouts, 'layout')\n collect(sources.components, 'component')\n collect(sources.emailTemplates ?? [], 'emailTemplate')\n collect(sources.systemEmails ?? [], 'systemEmail')\n return dependents\n}\n\n/**\n * Everything rendering inside a layout (AGL-703).\n *\n * Two kinds of dependent, both expressed by the same `layoutId` pointer:\n *\n * - **screens**, which name the layout they render inside;\n * - **other layouts**, since a layout can itself sit inside one. A nested\n * layout is a real dependent — deleting its parent unwraps every screen\n * underneath it — so leaving layouts out would report a parent layout as\n * used only by the screens that name it directly, and none of the ones\n * that reach it through a child.\n *\n * A layout never counts as its own dependent; `canNestLayout` refuses that,\n * and this refuses to report it even if stored data holds one.\n */\nexport function scanLayoutUsage(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): UsageDependent[] {\n if (!layoutId) return []\n const dependentsOf = (\n candidates: UsageCandidate[],\n type: 'screen' | 'layout',\n ) =>\n candidates\n .filter(\n (candidate) =>\n isLiveUsageCandidate(candidate) &&\n candidate.layoutId === layoutId &&\n candidate.id !== layoutId,\n )\n .map((candidate) => ({\n type,\n id: candidate.id,\n name: usageCandidateLabel(candidate),\n via: ['id' as const],\n ...(candidate.versionId ? { versionId: candidate.versionId } : {}),\n }))\n return [\n ...dependentsOf(screens, 'screen'),\n ...dependentsOf(layouts, 'layout'),\n ]\n}\n\n/**\n * Every live screen rendered inside `layoutId`, at ANY nesting depth\n * (AGL-1150).\n *\n * `scanLayoutUsage` answers one level. Layouts nest — a screen points at a\n * layout, which can point at a parent layout, and `compose-screen-nodes` walks\n * that whole chain when composing a page. So publishing a layout changes every\n * screen below it, not just the ones bound to it directly, and a cache drop\n * that only handles the direct level leaves the rest showing stale chrome for\n * the full revalidate window.\n *\n * Pure, and separate from the Firestore read, so the nesting behaviour is\n * testable without a database.\n *\n * Cycle-safe. `canNestLayout` refuses to create a cycle, but a document written\n * straight to Firestore is not bound by that, and a cycle here would hang a\n * publish request rather than surface anything.\n */\nexport function screenIdsUsingLayoutDeep(\n layoutId: string,\n screens: UsageCandidate[],\n layouts: UsageCandidate[] = [],\n): string[] {\n if (!layoutId) return []\n const screenIds = new Set<string>()\n const seenLayouts = new Set<string>([layoutId])\n let frontier = [layoutId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanLayoutUsage(id, screens, layouts)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (!seenLayouts.has(dependent.id)) {\n seenLayouts.add(dependent.id)\n next.push(dependent.id)\n }\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\n/**\n * Every live screen whose rendered output contains `componentId`, however\n * indirectly (AGL-1161).\n *\n * `scanComponentUsage` answers one level and returns three kinds of dependent.\n * Only one of them is a screen, and the other two both reach screens by routes\n * a single-level scan cannot see:\n *\n * - a **component** dependent nests the target inside itself, and that outer\n * component may itself only be used inside a third — so component→component\n * edges have to be followed to a fixed point;\n * - a **layout** dependent puts the component in page chrome, which every\n * screen under that layout renders. Layouts nest, so that is\n * `screenIdsUsingLayoutDeep`, not a direct `layoutId` match.\n *\n * Miss either and a publish reports success while some pages keep serving the\n * old component for the full revalidate window — the failure this whole arc\n * exists to remove, and the one that is hardest to notice because the pages\n * that ARE dropped update instantly.\n *\n * Pure, and separate from the Firestore read, so the closure is testable\n * without a database — the same split `screenIdsUsingLayoutDeep` uses.\n *\n * Cycle-safe. `composeReusableComponentNodes` would not survive a cycle, but a\n * document written straight to Firestore is not bound by what the editor\n * allows, and a cycle here would hang a publish rather than surface anything.\n */\nexport function screenIdsUsingComponentDeep(\n componentId: string,\n sources: UsageSources,\n): string[] {\n if (!componentId) return []\n const screenIds = new Set<string>()\n const seenComponents = new Set<string>([componentId])\n // Layouts are resolved through their own deep walk, so remember which ones\n // have already been expanded: two components in the same layout would\n // otherwise re-walk the whole layout tree once each.\n const seenLayouts = new Set<string>()\n let frontier = [componentId]\n\n while (frontier.length) {\n const next: string[] = []\n for (const id of frontier) {\n for (const dependent of scanComponentUsage(id, sources)) {\n if (dependent.type === 'screen') {\n screenIds.add(dependent.id)\n } else if (dependent.type === 'layout') {\n if (seenLayouts.has(dependent.id)) continue\n seenLayouts.add(dependent.id)\n // The layout itself renders no URL; the screens beneath it do.\n for (const screenId of screenIdsUsingLayoutDeep(\n dependent.id,\n sources.screens,\n sources.layouts,\n )) {\n screenIds.add(screenId)\n }\n } else if (\n dependent.type === 'component' &&\n !seenComponents.has(dependent.id)\n ) {\n seenComponents.add(dependent.id)\n next.push(dependent.id)\n }\n // An email — transactional, designed for a campaign, or one of the\n // platform's own — is not a page and nothing caches it, so it\n // contributes no screen and is not followed (AGL-3287, AGL-3318). A\n // transactional or platform email's id is a catalog key, and walking\n // it as a component would read a key as a definition that does not\n // exist.\n }\n }\n frontier = next\n }\n\n return [...screenIds]\n}\n\nexport interface UsageCandidateRead {\n candidates: UsageCandidate[]\n /**\n * The collection held more documents than `limit` allowed, so the scan below\n * it is INCOMPLETE.\n *\n * Returned rather than logged, because the two callers owe the user\n * different things: an advisory \"what would I break\" can show a partial\n * answer and say so, while a cache drop that silently scans a prefix reports\n * a successful publish and leaves real pages stale.\n */\n truncated: boolean\n}\n\n/**\n * One collection's documents, with published nodes attached when the scan\n * needs to search them.\n *\n * `limit` is a real bound, not a guess: it is fetched with one extra document\n * so exceeding it is DETECTED rather than assumed away. A caller that ignores\n * `truncated` is choosing to be wrong quietly.\n *\n * `emailTemplates` — the site's transactional email designs (AGL-3287) — has\n * the screens' shape: a parent holding the published `versionId`, the tree on\n * that version. The catalog is code and fixed, and a template document only\n * exists once somebody pressed Design, so most sites have none and the read\n * costs one empty query. Its documents carry no name, so each is labelled with\n * the catalog's.\n */\nexport async function readUsageCandidates(\n hostRef: FirebaseFirestore.DocumentReference,\n collectionName:\n | 'screens'\n | 'layouts'\n | 'components'\n | typeof TENANT_EMAIL_COLLECTION,\n options: { withNodes: boolean; limit: number },\n): Promise<UsageCandidateRead> {\n return readCandidatesIn(\n hostRef.collection(collectionName),\n collectionName,\n options,\n )\n}\n\n/**\n * The platform's own emails as a usage corpus (AGL-3318).\n *\n * They live at the ROOT, `systemEmailTemplates/{key}`, because they belong to\n * no site, with the shape a site's own emails have: the published `versionId`\n * on the template, the tree on that version, and no name but the catalog's.\n * Only a scan of the platform marketing site asks for them, since its email\n * blocks are the ones they place. Read under the same bound, and reporting\n * the same `truncated`, as a site's collections; the catalog is code, so a\n * document exists only for an email somebody designed, and the read is a\n * handful of documents.\n */\nexport async function readSystemEmailUsageCandidates(\n firestore: Pick<FirebaseFirestore.Firestore, 'collection'>,\n options: { limit: number },\n): Promise<UsageCandidateRead> {\n return readCandidatesIn(\n firestore.collection(SYSTEM_EMAIL_COLLECTION),\n SYSTEM_EMAIL_COLLECTION,\n { withNodes: true, limit: options.limit },\n )\n}\n\n/** The read both of the above make, over the collection each one names. */\nasync function readCandidatesIn(\n collection: FirebaseFirestore.CollectionReference,\n collectionName:\n | 'screens'\n | 'layouts'\n | 'components'\n | typeof TENANT_EMAIL_COLLECTION\n | typeof SYSTEM_EMAIL_COLLECTION,\n options: { withNodes: boolean; limit: number },\n): Promise<UsageCandidateRead> {\n const { withNodes, limit } = options\n // One over the limit: if the extra document comes back, there was more than\n // we are about to look at. Cheaper than a count() and exact.\n const docs = await collection.limit(limit + 1).get()\n const truncated = docs.size > limit\n const inScope = truncated ? docs.docs.slice(0, limit) : docs.docs\n\n const candidates = await Promise.all(\n inScope.map(async (docSnapshot) => {\n const versionId = docSnapshot.get('versionId')\n // Components keep their tree on the document; screens and layouts keep\n // it on the published version.\n //\n // BOTH reads are decoded (AGL-1223). A component document is msgpack\n // for anything promoted since components were compressed and a plain\n // map for everything older, and `decodeStoredNodes` returns a map\n // unchanged — so the branch below cannot be simplified back into a raw\n // read on either side.\n //\n // What rides on this: every consumer walks the value with\n // `Object.values`, which over a `Buffer` yields byte NUMBERS and\n // matches nothing. The answer that produces is \"used nowhere\" — for\n // `/api/hosts/where-used` an invitation to delete something a live page\n // renders, and for `/api/screens/revalidate` a publish that drops no\n // cache and leaves the old page served.\n const nodes =\n collectionName === 'components'\n ? decodeStoredNodes(docSnapshot.get('nodes'))\n : withNodes && versionId\n ? await docSnapshot.ref\n .collection('versions')\n .doc(String(versionId))\n .get()\n .then((version) => decodeStoredNodes(version.get('nodes')))\n .catch(() => null)\n : null\n return {\n id: docSnapshot.id,\n displayName:\n collectionName === TENANT_EMAIL_COLLECTION\n ? (getTenantEmail(docSnapshot.id)?.name ?? docSnapshot.id)\n : collectionName === SYSTEM_EMAIL_COLLECTION\n ? (getSystemEmailTemplate(docSnapshot.id)?.name ??\n docSnapshot.id)\n : docSnapshot.get('displayName'),\n name: docSnapshot.get('name'),\n deletedAt: docSnapshot.get('deletedAt'),\n nodes,\n ...(versionId ? { versionId: String(versionId) } : {}),\n ...(docSnapshot.get('layoutId')\n ? { layoutId: String(docSnapshot.get('layoutId')) }\n : {}),\n // Screens only, and only the screen scan reads it: a child screen is\n // a dependent of its parent, because its PATH is built from it\n // (AGL-703).\n ...(docSnapshot.get('parentId')\n ? { parentId: String(docSnapshot.get('parentId')) }\n : {}),\n // Screens only: a campaign email design is a screen document that is\n // never a page (AGL-3287).\n ...(collectionName === 'screens' && docSnapshot.get('kind')\n ? { kind: String(docSnapshot.get('kind')) }\n : {}),\n // Components only: the declared properties, whose Link defaults render\n // as links wherever an instance leaves one unset (AGL-2846). The same\n // document the tree came from, so it costs no read.\n ...(collectionName === 'components' &&\n Array.isArray(docSnapshot.get('props'))\n ? { props: docSnapshot.get('props') }\n : {}),\n } satisfies UsageCandidate\n }),\n )\n\n return { candidates, truncated }\n}\n\n/**\n * Every screen, layout and component of a site, with their node trees — the\n * corpus every tree-searching closure above walks.\n *\n * Each collection ONCE, in memory: a query per level would multiply round\n * trips by the nesting depth of the graph. Shared so two scans cannot read\n * different corpora under different bounds, and a change to one cannot quietly\n * narrow the other.\n */\nexport async function readUsageSources(\n hostRef: FirebaseFirestore.DocumentReference,\n limit: number,\n): Promise<{ candidates: UsageSources; truncated: boolean }> {\n const [screens, layouts, components] = await Promise.all([\n readUsageCandidates(hostRef, 'screens', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'layouts', { withNodes: true, limit }),\n readUsageCandidates(hostRef, 'components', { withNodes: true, limit }),\n ])\n return {\n candidates: {\n screens: screens.candidates,\n layouts: layouts.candidates,\n components: components.candidates,\n },\n truncated: screens.truncated || layouts.truncated || components.truncated,\n }\n}\n"],"names":["nodesReferenceComponent","decodeStoredNodes","getSystemEmailTemplate","SYSTEM_EMAIL_COLLECTION","getTenantEmail","TENANT_EMAIL_COLLECTION","usageCandidateLabel","candidate","String","displayName","name","id","isLiveUsageCandidate","deletedAt","scanComponentUsage","componentId","sources","dependents","collect","candidates","type","nodes","push","kind","via","versionId","screens","layouts","components","emailTemplates","systemEmails","scanLayoutUsage","layoutId","dependentsOf","filter","map","screenIdsUsingLayoutDeep","screenIds","Set","seenLayouts","frontier","length","next","dependent","add","has","screenIdsUsingComponentDeep","seenComponents","screenId","readUsageCandidates","hostRef","collectionName","options","readCandidatesIn","collection","readSystemEmailUsageCandidates","firestore","withNodes","limit","docs","get","truncated","size","inScope","slice","Promise","all","docSnapshot","ref","doc","then","version","catch","parentId","Array","isArray","props","readUsageSources"],"mappings":"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"}