@aglyn/tenant-data-admin 1.0.0-beta.188 → 1.0.0-beta.189
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.
|
|
3
|
+
"version": "1.0.0-beta.189",
|
|
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.
|
|
29
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-fbserver": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.189",
|
|
29
|
+
"@aglyn/shared-util-email": "1.0.0-beta.189",
|
|
30
|
+
"@aglyn/shared-util-fbserver": "1.0.0-beta.189",
|
|
31
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.189",
|
|
32
|
+
"@aglyn/shared-util-http": "1.0.0-beta.189",
|
|
33
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.189",
|
|
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
|
-
*
|
|
125
|
-
* and
|
|
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
|
-
*
|
|
50
|
-
* and
|
|
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
|
|
212
|
-
// and nothing caches it, so it
|
|
213
|
-
// followed (AGL-3287). A
|
|
214
|
-
//
|
|
215
|
-
// does not
|
|
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
|
|
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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"}
|