@aglyn/plugins-data 1.0.0-beta.220 → 1.0.0-beta.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +9 -6
  2. package/package.json +10 -10
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +1 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
  7. package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
  8. package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
  9. package/src/lib/artifact/dataset-schema.d.ts +136 -0
  10. package/src/lib/artifact/dataset-schema.js +232 -0
  11. package/src/lib/artifact/dataset-schema.js.map +1 -0
  12. package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
  13. package/src/lib/components/dataset-record-dialog.component.js +1 -1
  14. package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
  15. package/src/lib/components/dataset-record-filter.d.ts +1 -1
  16. package/src/lib/components/dataset-record-filter.js +2 -2
  17. package/src/lib/components/dataset-record-filter.js.map +1 -1
  18. package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
  19. package/src/lib/components/dataset-schema-dialog.component.js +55 -23
  20. package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
  21. package/src/lib/components/host-datasets-card.component.js +91 -403
  22. package/src/lib/components/host-datasets-card.component.js.map +1 -1
  23. package/src/lib/constants/bundle-common.d.ts +12 -0
  24. package/src/lib/constants/bundle-common.js +13 -0
  25. package/src/lib/constants/bundle-common.js.map +1 -1
  26. package/src/lib/declarations.console-server.d.ts +16 -1
  27. package/src/lib/declarations.console-server.js +34 -2
  28. package/src/lib/declarations.console-server.js.map +1 -1
  29. package/src/lib/declarations.server.d.ts +13 -0
  30. package/src/lib/declarations.server.js +48 -2
  31. package/src/lib/declarations.server.js.map +1 -1
  32. package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
  33. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
  34. package/src/lib/model/dataset-models.d.ts +392 -0
  35. package/src/lib/model/dataset-models.js +806 -0
  36. package/src/lib/model/dataset-models.js.map +1 -0
  37. package/src/lib/model/dataset-record-view.d.ts +1 -1
  38. package/src/lib/model/dataset-record-view.js +1 -2
  39. package/src/lib/model/dataset-record-view.js.map +1 -1
  40. package/src/lib/model/dataset-record.js +2 -2
  41. package/src/lib/model/dataset-record.js.map +1 -1
  42. package/src/lib/model/datasets.d.ts +162 -0
  43. package/src/lib/model/datasets.js +227 -0
  44. package/src/lib/model/datasets.js.map +1 -0
  45. package/src/lib/plugin.d.ts +4 -0
  46. package/src/lib/plugin.js +40 -0
  47. package/src/lib/plugin.js.map +1 -1
  48. package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
  49. package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
  50. package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
  51. package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
  52. package/src/lib/record-pages/record-page-read.server.js +175 -0
  53. package/src/lib/record-pages/record-page-read.server.js.map +1 -0
  54. package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
  55. package/src/lib/record-pages/record-page-resolver.server.js +131 -0
  56. package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
  57. package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
  58. package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
  59. package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
  60. package/src/lib/record-pages/record-page-source.d.ts +42 -0
  61. package/src/lib/record-pages/record-page-source.js +176 -0
  62. package/src/lib/record-pages/record-page-source.js.map +1 -0
  63. package/src/lib/record-pages/record-pages-route.d.ts +42 -0
  64. package/src/lib/record-pages/record-pages-route.js +348 -0
  65. package/src/lib/record-pages/record-pages-route.js.map +1 -0
  66. package/src/lib/record-pages/record-pages.d.ts +218 -0
  67. package/src/lib/record-pages/record-pages.js +351 -0
  68. package/src/lib/record-pages/record-pages.js.map +1 -0
  69. package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
  70. package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
  71. package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
  72. package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
  73. package/src/lib/record-pages/record-template-section.component.js +491 -0
  74. package/src/lib/record-pages/record-template-section.component.js.map +1 -0
  75. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
  76. package/src/lib/repeat/dataset-repeat-rows.js +30 -12
  77. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  78. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
  79. package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
  80. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
  81. package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
  82. package/src/lib/repeat/repeat-record-pages.js +64 -0
  83. package/src/lib/repeat/repeat-record-pages.js.map +1 -0
  84. package/src/lib/server/announce-dataset-records.d.ts +1 -1
  85. package/src/lib/server/announce-dataset-records.js +1 -1
  86. package/src/lib/server/announce-dataset-records.js.map +1 -1
  87. package/src/lib/server/api-v1/datasets.js +5 -3
  88. package/src/lib/server/api-v1/datasets.js.map +1 -1
  89. package/src/lib/server/dataset-figures.js +2 -2
  90. package/src/lib/server/dataset-figures.js.map +1 -1
  91. package/src/lib/server/dataset-live-pages.d.ts +147 -0
  92. package/src/lib/server/dataset-live-pages.js +436 -0
  93. package/src/lib/server/dataset-live-pages.js.map +1 -0
  94. package/src/lib/server/dataset-steps.server.d.ts +45 -0
  95. package/src/lib/server/dataset-steps.server.js +268 -0
  96. package/src/lib/server/dataset-steps.server.js.map +1 -0
  97. package/src/lib/server/datasets-route.d.ts +2 -0
  98. package/src/lib/server/datasets-route.js +102 -6
  99. package/src/lib/server/datasets-route.js.map +1 -1
  100. package/src/lib/server/resolve-dataset.d.ts +40 -0
  101. package/src/lib/server/resolve-dataset.js +64 -0
  102. package/src/lib/server/resolve-dataset.js.map +1 -0
  103. package/src/lib/server.d.ts +3 -2
  104. package/src/lib/server.js +8 -9
  105. package/src/lib/server.js.map +1 -1
  106. package/src/lib/site-bundle/datasets-package.d.ts +42 -0
  107. package/src/lib/site-bundle/datasets-package.js +95 -0
  108. package/src/lib/site-bundle/datasets-package.js.map +1 -0
  109. package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
  110. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
  111. package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
  112. package/src/lib/tenant-api.js +30 -0
  113. package/src/lib/tenant-api.js.map +1 -0
  114. package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
  115. package/src/lib/transfer/dataset-transfer-key.js +31 -0
  116. package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
  117. package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
  118. package/src/lib/transfer/dataset-transfer-model.js +536 -0
  119. package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
  120. package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
  121. package/src/lib/transfer/dataset-transfer-resource.js +41 -0
  122. package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
  123. package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
  124. package/src/lib/transfer/dataset-transfer.server.js +930 -0
  125. package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
  126. package/src/lib/model/dataset-csv.d.ts +0 -36
  127. package/src/lib/model/dataset-csv.js +0 -85
  128. package/src/lib/model/dataset-csv.js.map +0 -1
  129. package/src/lib/model/dataset-io.d.ts +0 -44
  130. package/src/lib/model/dataset-io.js +0 -85
  131. package/src/lib/model/dataset-io.js.map +0 -1
  132. package/src/lib/model/index.js.map +0 -1
  133. package/src/lib/server/datasets-export-route.d.ts +0 -57
  134. package/src/lib/server/datasets-export-route.js +0 -229
  135. package/src/lib/server/datasets-export-route.js.map +0 -1
@@ -0,0 +1,175 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { visibleToHost } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin, orgDataQueryForHost } from "@aglyn/tenant-data-admin";
19
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
20
+ import { datasetFilterValuePath, effectiveDatasetModel, repeatRowsModelOf } from "../model/dataset-models.js";
21
+ import { datasetDisplayName, sortDatasetRecords } from "../model/datasets.js";
22
+ import { DATASET_RECORD_PAGES_COLLECTION, RECORD_PAGES_MAX_PER_SITE, parseRecordPageBinding, recordAddressOf, referencedRecordIds } from "./record-pages.js";
23
+ /**
24
+ * A site's record templates (AGL-3475), read once per render cache window and
25
+ * dropped with everything else the site caches under `tenant-data:{hostId}`,
26
+ * which every binding change busts.
27
+ *
28
+ * Asked on every request no published page claims, so it is one small cached
29
+ * read, bounded by {@link RECORD_PAGES_MAX_PER_SITE}. A site with none caches
30
+ * the empty list, which is the answer for almost every site.
31
+ */ export async function readSiteRecordPageBindings(hostId) {
32
+ const read = async ()=>{
33
+ const snapshot = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection(DATASET_RECORD_PAGES_COLLECTION).limit(RECORD_PAGES_MAX_PER_SITE).get();
34
+ return snapshot.docs.map((doc)=>parseRecordPageBinding(doc.id, doc.data())).filter((binding)=>binding != null);
35
+ };
36
+ try {
37
+ return await withRenderCache({
38
+ key: [
39
+ 'tenant-record-pages',
40
+ 'v1',
41
+ hostId
42
+ ],
43
+ revalidate: PUBLISHED_SITE_DATA_TTL_SECONDS,
44
+ tags: [
45
+ tenantDataTag(hostId)
46
+ ],
47
+ read
48
+ });
49
+ } catch (error) {
50
+ console.error('record page bindings: cached read failed', error);
51
+ return read();
52
+ }
53
+ }
54
+ /**
55
+ * The dataset a binding names, when THIS site may see it — the rule the
56
+ * published repeat reader applies (`visibleTo`, AGL-1039), checked on the
57
+ * document because a keyed read bypasses the scoped query. A deleted dataset,
58
+ * or one no longer shared with the site, is `null`, and its record pages stop
59
+ * serving rather than serving what the site may no longer show.
60
+ */ export async function readSiteDataset(hostId, datasetId) {
61
+ var _ref_parent_parent, _ref_parent;
62
+ if (!datasetId || datasetId.includes('/')) return null;
63
+ const { ref } = await orgDataQueryForHost(hostId, 'datasets');
64
+ const snapshot = await ref.doc(datasetId).get();
65
+ const orgScoped = ((_ref_parent = ref.parent) == null ? void 0 : (_ref_parent_parent = _ref_parent.parent) == null ? void 0 : _ref_parent_parent.id) === 'orgs';
66
+ if (!snapshot.exists || snapshot.get('deletedAt') || orgScoped && !visibleToHost(snapshot.get('visibleTo'), hostId)) {
67
+ return null;
68
+ }
69
+ const data = snapshot.data();
70
+ return {
71
+ id: snapshot.id,
72
+ name: datasetDisplayName(data),
73
+ model: effectiveDatasetModel(data),
74
+ ref: snapshot.ref
75
+ };
76
+ }
77
+ /** How many records the indexed lookup reads for one address. */ const ADDRESS_LOOKUP_LIMIT = 10;
78
+ /**
79
+ * The record a binding serves at `address`, or `null` — an unknown address,
80
+ * a dataset the site may not see, a slug field the model no longer has.
81
+ *
82
+ * Found through `filterValues.<slugField>`, the equality index every record
83
+ * write keeps (AGL-3321): a page address is stored normalized, which is
84
+ * exactly the key a plain text value's entry holds, so the lookup needs no
85
+ * index of its own. The candidates are then matched on the stored value
86
+ * itself, and when two records share an address the one first in the
87
+ * dataset's own order serves — the order a repeat lists them in.
88
+ */ export async function readRecordPageRecord(hostId, binding, address) {
89
+ const read = ()=>readRecordUncached(hostId, binding, address);
90
+ try {
91
+ var _ref;
92
+ // `null` is not cached (the cache stores only a defined answer), so an
93
+ // address that starts existing is found on its next request.
94
+ return (_ref = await withRenderCache({
95
+ key: [
96
+ 'tenant-record-page',
97
+ 'v1',
98
+ hostId,
99
+ binding.datasetId,
100
+ binding.slugField,
101
+ address
102
+ ],
103
+ revalidate: PUBLISHED_SITE_DATA_TTL_SECONDS,
104
+ tags: [
105
+ tenantDataTag(hostId)
106
+ ],
107
+ read: async ()=>{
108
+ var _ref;
109
+ return (_ref = await read()) != null ? _ref : undefined;
110
+ }
111
+ })) != null ? _ref : null;
112
+ } catch (error) {
113
+ console.error('record page: cached read failed', error);
114
+ return read();
115
+ }
116
+ }
117
+ async function readRecordUncached(hostId, binding, address) {
118
+ var _match_values;
119
+ const dataset = await readSiteDataset(hostId, binding.datasetId);
120
+ if (!dataset) return null;
121
+ const field = dataset.model.fields[binding.slugField];
122
+ const path = datasetFilterValuePath(binding.slugField);
123
+ if (!field || field.type !== 'text' || !path) return null;
124
+ const snapshot = await dataset.ref.collection('records').where(path, '==', address).limit(ADDRESS_LOOKUP_LIMIT).get();
125
+ const [match] = sortDatasetRecords(snapshot.docs.map((doc)=>_extends({
126
+ $id: doc.id
127
+ }, doc.data())).filter((record)=>recordAddressOf(record.values, binding.slugField) === address));
128
+ if (!match) return null;
129
+ const record = _extends({}, (_match_values = match.values) != null ? _match_values : {}, {
130
+ $id: match.$id
131
+ });
132
+ return {
133
+ dataset: {
134
+ id: dataset.id,
135
+ name: dataset.name,
136
+ model: dataset.model
137
+ },
138
+ record,
139
+ datasetsByKey: await readReferenceHops(hostId, dataset.model, record),
140
+ updatedAt: match.updatedAt
141
+ };
142
+ }
143
+ /**
144
+ * The records a routed record's reference fields point at, keyed by the
145
+ * dataset each lives in — just those records, by id, rather than the first
146
+ * hundred rows a repeat would read. A target the site may not see is left
147
+ * out, and its token stays as written, as in a repeat.
148
+ */ async function readReferenceHops(hostId, model, record) {
149
+ const wanted = referencedRecordIds(repeatRowsModelOf(model), record);
150
+ const answer = {};
151
+ await Promise.all([
152
+ ...wanted
153
+ ].map(async ([datasetId, ids])=>{
154
+ try {
155
+ const target = await readSiteDataset(hostId, datasetId);
156
+ if (!target) return;
157
+ const docs = await Promise.all([
158
+ ...ids
159
+ ].map((id)=>target.ref.collection('records').doc(id).get()));
160
+ answer[datasetId] = {
161
+ records: docs.filter((doc)=>doc.exists).map((doc)=>{
162
+ var _doc_data_values;
163
+ return _extends({}, (_doc_data_values = doc.data().values) != null ? _doc_data_values : {}, {
164
+ $id: doc.id
165
+ });
166
+ })
167
+ };
168
+ } catch (error) {
169
+ console.error('record page: reference hop failed', error);
170
+ }
171
+ }));
172
+ return answer;
173
+ }
174
+
175
+ //# sourceMappingURL=record-page-read.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-page-read.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type RepeatableDataset, visibleToHost } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, orgDataQueryForHost } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport {\n datasetFilterValuePath,\n type DatasetModel,\n effectiveDatasetModel,\n repeatRowsModelOf,\n} from '../model/dataset-models'\nimport {\n datasetDisplayName,\n type HostDataset,\n type HostDatasetRecord,\n sortDatasetRecords,\n} from '../model/datasets'\nimport {\n DATASET_RECORD_PAGES_COLLECTION,\n type DatasetRecordPageBinding,\n RECORD_PAGES_MAX_PER_SITE,\n parseRecordPageBinding,\n recordAddressOf,\n referencedRecordIds,\n} from './record-pages'\n\n/**\n * A site's record templates (AGL-3475), read once per render cache window and\n * dropped with everything else the site caches under `tenant-data:{hostId}`,\n * which every binding change busts.\n *\n * Asked on every request no published page claims, so it is one small cached\n * read, bounded by {@link RECORD_PAGES_MAX_PER_SITE}. A site with none caches\n * the empty list, which is the answer for almost every site.\n */\nexport async function readSiteRecordPageBindings(\n hostId: string,\n): Promise<DatasetRecordPageBinding[]> {\n const read = async () => {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection(DATASET_RECORD_PAGES_COLLECTION)\n .limit(RECORD_PAGES_MAX_PER_SITE)\n .get()\n return snapshot.docs\n .map((doc) => parseRecordPageBinding(doc.id, doc.data()))\n .filter((binding): binding is DatasetRecordPageBinding => binding != null)\n }\n try {\n return await withRenderCache({\n key: ['tenant-record-pages', 'v1', hostId],\n revalidate: PUBLISHED_SITE_DATA_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('record page bindings: cached read failed', error)\n return read()\n }\n}\n\n/** A dataset this site may see, with its model, or `null`. */\nexport interface SiteDataset {\n id: string\n name: string\n model: DatasetModel\n ref: FirebaseFirestore.DocumentReference\n}\n\n/**\n * The dataset a binding names, when THIS site may see it — the rule the\n * published repeat reader applies (`visibleTo`, AGL-1039), checked on the\n * document because a keyed read bypasses the scoped query. A deleted dataset,\n * or one no longer shared with the site, is `null`, and its record pages stop\n * serving rather than serving what the site may no longer show.\n */\nexport async function readSiteDataset(\n hostId: string,\n datasetId: string,\n): Promise<SiteDataset | null> {\n if (!datasetId || datasetId.includes('/')) return null\n const { ref } = await orgDataQueryForHost(hostId, 'datasets')\n const snapshot = await ref.doc(datasetId).get()\n const orgScoped = ref.parent?.parent?.id === 'orgs'\n if (\n !snapshot.exists ||\n snapshot.get('deletedAt') ||\n (orgScoped && !visibleToHost(snapshot.get('visibleTo'), hostId))\n ) {\n return null\n }\n const data = snapshot.data() as HostDataset\n return {\n id: snapshot.id,\n name: datasetDisplayName(data),\n model: effectiveDatasetModel(data),\n ref: snapshot.ref,\n }\n}\n\n/** One routed record, ready for the template: its row and its one hop. */\nexport interface RecordPageRecord {\n dataset: { id: string; name: string; model: DatasetModel }\n /** The record's values, with `$id` inside like a repeated row. */\n record: Record<string, unknown>\n /** The datasets its reference fields point into, for one-hop tokens. */\n datasetsByKey: Record<string, RepeatableDataset>\n /** The record's write time, for the sitemap's and the head's dates. */\n updatedAt?: unknown\n}\n\n/** How many records the indexed lookup reads for one address. */\nconst ADDRESS_LOOKUP_LIMIT = 10\n\n/**\n * The record a binding serves at `address`, or `null` — an unknown address,\n * a dataset the site may not see, a slug field the model no longer has.\n *\n * Found through `filterValues.<slugField>`, the equality index every record\n * write keeps (AGL-3321): a page address is stored normalized, which is\n * exactly the key a plain text value's entry holds, so the lookup needs no\n * index of its own. The candidates are then matched on the stored value\n * itself, and when two records share an address the one first in the\n * dataset's own order serves — the order a repeat lists them in.\n */\nexport async function readRecordPageRecord(\n hostId: string,\n binding: DatasetRecordPageBinding,\n address: string,\n): Promise<RecordPageRecord | null> {\n const read = () => readRecordUncached(hostId, binding, address)\n try {\n // `null` is not cached (the cache stores only a defined answer), so an\n // address that starts existing is found on its next request.\n return (\n (await withRenderCache({\n key: [\n 'tenant-record-page',\n 'v1',\n hostId,\n binding.datasetId,\n binding.slugField,\n address,\n ],\n revalidate: PUBLISHED_SITE_DATA_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read: async () => (await read()) ?? undefined,\n })) ?? null\n )\n } catch (error) {\n console.error('record page: cached read failed', error)\n return read()\n }\n}\n\nasync function readRecordUncached(\n hostId: string,\n binding: DatasetRecordPageBinding,\n address: string,\n): Promise<RecordPageRecord | null> {\n const dataset = await readSiteDataset(hostId, binding.datasetId)\n if (!dataset) return null\n const field = dataset.model.fields[binding.slugField]\n const path = datasetFilterValuePath(binding.slugField)\n if (!field || field.type !== 'text' || !path) return null\n const snapshot = await dataset.ref\n .collection('records')\n .where(path, '==', address)\n .limit(ADDRESS_LOOKUP_LIMIT)\n .get()\n const [match] = sortDatasetRecords(\n snapshot.docs\n .map((doc) => ({ $id: doc.id, ...(doc.data() as HostDatasetRecord) }))\n .filter((record) => recordAddressOf(record.values, binding.slugField) === address),\n )\n if (!match) return null\n const record = { ...(match.values ?? {}), $id: match.$id }\n return {\n dataset: { id: dataset.id, name: dataset.name, model: dataset.model },\n record,\n datasetsByKey: await readReferenceHops(hostId, dataset.model, record),\n updatedAt: (match as { updatedAt?: unknown }).updatedAt,\n }\n}\n\n/**\n * The records a routed record's reference fields point at, keyed by the\n * dataset each lives in — just those records, by id, rather than the first\n * hundred rows a repeat would read. A target the site may not see is left\n * out, and its token stays as written, as in a repeat.\n */\nasync function readReferenceHops(\n hostId: string,\n model: DatasetModel,\n record: Record<string, unknown>,\n): Promise<Record<string, RepeatableDataset>> {\n const wanted = referencedRecordIds(repeatRowsModelOf(model), record)\n const answer: Record<string, RepeatableDataset> = {}\n await Promise.all(\n [...wanted].map(async ([datasetId, ids]) => {\n try {\n const target = await readSiteDataset(hostId, datasetId)\n if (!target) return\n const docs = await Promise.all(\n [...ids].map((id) => target.ref.collection('records').doc(id).get()),\n )\n answer[datasetId] = {\n records: docs\n .filter((doc) => doc.exists)\n .map((doc) => ({\n ...((doc.data() as HostDatasetRecord).values ?? {}),\n $id: doc.id,\n })),\n }\n } catch (error) {\n console.error('record page: reference hop failed', error)\n }\n }),\n )\n return answer\n}\n"],"names":["visibleToHost","firebaseAdmin","orgDataQueryForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","datasetFilterValuePath","effectiveDatasetModel","repeatRowsModelOf","datasetDisplayName","sortDatasetRecords","DATASET_RECORD_PAGES_COLLECTION","RECORD_PAGES_MAX_PER_SITE","parseRecordPageBinding","recordAddressOf","referencedRecordIds","readSiteRecordPageBindings","hostId","read","snapshot","app","firestore","collection","doc","limit","get","docs","map","id","data","filter","binding","key","revalidate","tags","error","console","readSiteDataset","datasetId","ref","includes","orgScoped","parent","exists","name","model","ADDRESS_LOOKUP_LIMIT","readRecordPageRecord","address","readRecordUncached","slugField","undefined","match","dataset","field","fields","path","type","where","$id","record","values","datasetsByKey","readReferenceHops","updatedAt","wanted","answer","Promise","all","ids","target","records"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type SitePageResolver } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Record pages on the published site (AGL-3475): `/{base}/{address}` renders
20
+ * the binding's template page with the routed record in scope.
21
+ *
22
+ * Registered on the platform's site page resolver seam, the one commerce
23
+ * serves `/products/{slug}` through, so it runs only for a path no published
24
+ * page claims — a page an author publishes at `/services/roofing` always wins
25
+ * over the record of that address. A path that matches no binding, an address
26
+ * no record holds, a dataset the site may no longer see, and a template that
27
+ * is no longer a template or has never been published all answer `undefined`,
28
+ * and the request carries on down the loader's chain to the site's 404.
29
+ *
30
+ * A site that has switched the Data plugin off serves none of them.
31
+ *
32
+ * The template must be a `kind: 'template'` screen: that stamp is what keeps
33
+ * its own address from serving raw `{{item.*}}` tokens, so a binding whose
34
+ * page has been made a page again serves nothing until it is a template
35
+ * again.
36
+ */
37
+ export declare const recordPageResolver: SitePageResolver;
@@ -0,0 +1,131 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { SCREEN_KIND_TEMPLATE, isHostPluginEnabled } from "@aglyn/aglyn/server";
18
+ import composeScreenNodes from "@aglyn/tenant-runtime/compose-screen-nodes";
19
+ import getScreen from "@aglyn/tenant-runtime/get-screen";
20
+ import { collectSocialImageFacts } from "@aglyn/tenant-runtime/social-image-facts";
21
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
22
+ import { repeatRowsModelOf } from "../model/dataset-models.js";
23
+ import { readRecordPageRecord, readSiteRecordPageBindings } from "./record-page-read.server.js";
24
+ import { matchRecordPagePath, recordPageHead, recordPagePath, recordPageRoute, withRecordPageUrls } from "./record-pages.js";
25
+ /**
26
+ * Record pages on the published site (AGL-3475): `/{base}/{address}` renders
27
+ * the binding's template page with the routed record in scope.
28
+ *
29
+ * Registered on the platform's site page resolver seam, the one commerce
30
+ * serves `/products/{slug}` through, so it runs only for a path no published
31
+ * page claims — a page an author publishes at `/services/roofing` always wins
32
+ * over the record of that address. A path that matches no binding, an address
33
+ * no record holds, a dataset the site may no longer see, and a template that
34
+ * is no longer a template or has never been published all answer `undefined`,
35
+ * and the request carries on down the loader's chain to the site's 404.
36
+ *
37
+ * A site that has switched the Data plugin off serves none of them.
38
+ *
39
+ * The template must be a `kind: 'template'` screen: that stamp is what keeps
40
+ * its own address from serving raw `{{item.*}}` tokens, so a binding whose
41
+ * page has been made a page again serves nothing until it is a template
42
+ * again.
43
+ */ export const recordPageResolver = async ({ hostId, host, org, path })=>{
44
+ var _withRecordPageUrls_, _template_seo, _head_description, _head_image;
45
+ var _template_seo1, _host_seo, _template_seo2, _template_seo3;
46
+ // A site that switched Data off serves no record pages (the plugin's
47
+ // `publishedSiteImpact: routes`).
48
+ if (!isHostPluginEnabled(org, host, BUNDLE_ID)) return undefined;
49
+ const bindings = await readSiteRecordPageBindings(hostId);
50
+ if (!bindings.length) return undefined;
51
+ const match = matchRecordPagePath(bindings, path);
52
+ if (!match) return undefined;
53
+ const { binding, address } = match;
54
+ const [routed, templateRes] = await Promise.all([
55
+ readRecordPageRecord(hostId, binding, address),
56
+ getScreen({
57
+ hostId,
58
+ screenId: binding.screenId,
59
+ allowTemplate: true
60
+ })
61
+ ]);
62
+ const template = templateRes.screen;
63
+ if (!routed || !template || template.kind !== SCREEN_KIND_TEMPLATE) {
64
+ return undefined;
65
+ }
66
+ const head = recordPageHead(binding, routed.dataset.model, routed.record);
67
+ const pagePath = recordPagePath(binding.base, address);
68
+ // The record's own image leads, then the template's, then the site's
69
+ // (AGL-2850): their documents are read in the page's one facts batch.
70
+ const card = collectSocialImageFacts([
71
+ head.image,
72
+ (_template_seo1 = template['seo']) == null ? void 0 : _template_seo1.image,
73
+ host == null ? void 0 : (_host_seo = host.seo) == null ? void 0 : _host_seo.image
74
+ ]);
75
+ const nodes = await composeScreenNodes({
76
+ hostId,
77
+ screenId: binding.screenId,
78
+ screen: template,
79
+ socialImages: card.socialImages,
80
+ host,
81
+ record: {
82
+ // The record's own page as `url` too, as its row in a repeat has, so a
83
+ // share link on the template reads {{item.url}} like a card does.
84
+ record: (_withRecordPageUrls_ = withRecordPageUrls([
85
+ routed.record
86
+ ], binding, routed.dataset.model)[0]) != null ? _withRecordPageUrls_ : routed.record,
87
+ model: repeatRowsModelOf(routed.dataset.model),
88
+ datasetsByKey: routed.datasetsByKey
89
+ },
90
+ // Named as the record template it is if the page review holds it
91
+ // (AGL-3374).
92
+ page: {
93
+ template: {
94
+ role: 'entry',
95
+ route: recordPageRoute(binding.base),
96
+ collectionName: routed.dataset.name || 'Dataset',
97
+ entryPath: pagePath,
98
+ fallback: 'not-found'
99
+ }
100
+ }
101
+ });
102
+ if (!nodes) return undefined;
103
+ return {
104
+ props: JSON.parse(JSON.stringify(_extends({
105
+ data: {
106
+ host,
107
+ screen: {
108
+ data: _extends({}, template, head.name ? {
109
+ displayName: head.name
110
+ } : {}, {
111
+ seo: _extends({}, (_template_seo = template['seo']) != null ? _template_seo : {}, {
112
+ // Only the record's own: inheriting the template's title
113
+ // would give every record page the same `<title>`.
114
+ title: head.title,
115
+ description: (_head_description = head.description) != null ? _head_description : (_template_seo2 = template['seo']) == null ? void 0 : _template_seo2.description,
116
+ image: (_head_image = head.image) != null ? _head_image : (_template_seo3 = template['seo']) == null ? void 0 : _template_seo3.image
117
+ })
118
+ })
119
+ }
120
+ },
121
+ nodes,
122
+ // The address this page answers at, in the routing map's form, so
123
+ // the head's canonical and breadcrumb name it rather than the
124
+ // template's own (which serves nothing).
125
+ routePath: pagePath.slice(1)
126
+ }, card.collected()))),
127
+ revalidate: 60
128
+ };
129
+ };
130
+
131
+ //# sourceMappingURL=record-page-resolver.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-page-resolver.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n SCREEN_KIND_TEMPLATE,\n type SitePageResolver,\n isHostPluginEnabled,\n} from '@aglyn/aglyn/server'\nimport composeScreenNodes from '@aglyn/tenant-runtime/compose-screen-nodes'\nimport getScreen from '@aglyn/tenant-runtime/get-screen'\nimport { collectSocialImageFacts } from '@aglyn/tenant-runtime/social-image-facts'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { repeatRowsModelOf } from '../model/dataset-models'\nimport { readRecordPageRecord, readSiteRecordPageBindings } from './record-page-read.server'\nimport {\n matchRecordPagePath,\n recordPageHead,\n recordPagePath,\n recordPageRoute,\n withRecordPageUrls,\n} from './record-pages'\n\n/**\n * Record pages on the published site (AGL-3475): `/{base}/{address}` renders\n * the binding's template page with the routed record in scope.\n *\n * Registered on the platform's site page resolver seam, the one commerce\n * serves `/products/{slug}` through, so it runs only for a path no published\n * page claims — a page an author publishes at `/services/roofing` always wins\n * over the record of that address. A path that matches no binding, an address\n * no record holds, a dataset the site may no longer see, and a template that\n * is no longer a template or has never been published all answer `undefined`,\n * and the request carries on down the loader's chain to the site's 404.\n *\n * A site that has switched the Data plugin off serves none of them.\n *\n * The template must be a `kind: 'template'` screen: that stamp is what keeps\n * its own address from serving raw `{{item.*}}` tokens, so a binding whose\n * page has been made a page again serves nothing until it is a template\n * again.\n */\nexport const recordPageResolver: SitePageResolver = async ({\n hostId,\n host,\n org,\n path,\n}) => {\n // A site that switched Data off serves no record pages (the plugin's\n // `publishedSiteImpact: routes`).\n if (!isHostPluginEnabled(org, host, BUNDLE_ID)) return undefined\n const bindings = await readSiteRecordPageBindings(hostId)\n if (!bindings.length) return undefined\n const match = matchRecordPagePath(bindings, path)\n if (!match) return undefined\n const { binding, address } = match\n\n const [routed, templateRes] = await Promise.all([\n readRecordPageRecord(hostId, binding, address),\n getScreen({ hostId, screenId: binding.screenId, allowTemplate: true }),\n ])\n const template = templateRes.screen as\n | (Record<string, any> & { kind?: unknown })\n | undefined\n if (!routed || !template || template.kind !== SCREEN_KIND_TEMPLATE) {\n return undefined\n }\n\n const head = recordPageHead(binding, routed.dataset.model, routed.record)\n const pagePath = recordPagePath(binding.base, address)\n // The record's own image leads, then the template's, then the site's\n // (AGL-2850): their documents are read in the page's one facts batch.\n const card = collectSocialImageFacts([\n head.image,\n template['seo']?.image,\n host?.seo?.image,\n ])\n const nodes = await composeScreenNodes({\n hostId,\n screenId: binding.screenId,\n screen: template as never,\n socialImages: card.socialImages,\n host,\n record: {\n // The record's own page as `url` too, as its row in a repeat has, so a\n // share link on the template reads {{item.url}} like a card does.\n record:\n withRecordPageUrls([routed.record], binding, routed.dataset.model)[0] ??\n routed.record,\n model: repeatRowsModelOf(routed.dataset.model),\n datasetsByKey: routed.datasetsByKey,\n },\n // Named as the record template it is if the page review holds it\n // (AGL-3374).\n page: {\n template: {\n role: 'entry',\n route: recordPageRoute(binding.base),\n collectionName: routed.dataset.name || 'Dataset',\n entryPath: pagePath,\n fallback: 'not-found',\n },\n },\n })\n if (!nodes) return undefined\n\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n ...template,\n // The record NAMES this page, as a product names its page: the\n // template's own name describes none of the records it draws.\n // A name takes the site title after it, while an authored\n // search title is used as written (AGL-1341).\n ...(head.name ? { displayName: head.name } : {}),\n seo: {\n ...(template['seo'] ?? {}),\n // Only the record's own: inheriting the template's title\n // would give every record page the same `<title>`.\n title: head.title,\n description: head.description ?? template['seo']?.description,\n image: head.image ?? template['seo']?.image,\n },\n },\n },\n },\n nodes,\n // The address this page answers at, in the routing map's form, so\n // the head's canonical and breadcrumb name it rather than the\n // template's own (which serves nothing).\n routePath: pagePath.slice(1),\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n}\n"],"names":["SCREEN_KIND_TEMPLATE","isHostPluginEnabled","composeScreenNodes","getScreen","collectSocialImageFacts","BUNDLE_ID","repeatRowsModelOf","readRecordPageRecord","readSiteRecordPageBindings","matchRecordPagePath","recordPageHead","recordPagePath","recordPageRoute","withRecordPageUrls","recordPageResolver","hostId","host","org","path","template","head","undefined","bindings","length","match","binding","address","routed","templateRes","Promise","all","screenId","allowTemplate","screen","kind","dataset","model","record","pagePath","base","card","image","seo","nodes","socialImages","datasetsByKey","page","role","route","collectionName","name","entryPath","fallback","props","JSON","parse","stringify","data","displayName","title","description","routePath","slice","collected","revalidate"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginSitemapReader } from '@aglyn/aglyn/plugin-manager/plugin-sitemap-readers';
18
+ /** The sitemap family record pages are listed in: `records-{key}`. */
19
+ export declare const RECORD_PAGES_SITEMAP_SECTION = "records";
20
+ /**
21
+ * A binding's child key: its base with each `/` as `--`, which a base segment
22
+ * can never hold (`services/residential` → `services--residential`), so the
23
+ * key names exactly one base.
24
+ */
25
+ export declare function recordPagesSitemapKey(base: string): string;
26
+ /**
27
+ * Record pages in the sitemap (AGL-3475): one child per record template, sized
28
+ * by its dataset's record count and paged by record id, so a page number names
29
+ * the same records on every fetch. A record with no address has no page and
30
+ * is left out of the file that would have held it.
31
+ */
32
+ export declare const recordPagesSitemapReader: PluginSitemapReader;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { SCREEN_KIND_TEMPLATE, isHostPluginEnabled, isScreenIndexable } from "@aglyn/aglyn/server";
17
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ import { FieldPath } from "firebase-admin/firestore";
19
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
20
+ import { humanizeDatasetFieldId } from "../model/dataset-models.js";
21
+ import { readSiteDataset, readSiteRecordPageBindings } from "./record-page-read.server.js";
22
+ import { recordAddressOf, recordPagePath } from "./record-pages.js";
23
+ /** The sitemap family record pages are listed in: `records-{key}`. */ export const RECORD_PAGES_SITEMAP_SECTION = 'records';
24
+ /**
25
+ * A binding's child key: its base with each `/` as `--`, which a base segment
26
+ * can never hold (`services/residential` → `services--residential`), so the
27
+ * key names exactly one base.
28
+ */ export function recordPagesSitemapKey(base) {
29
+ return base.replace(/\//g, '--');
30
+ }
31
+ /**
32
+ * The site's record templates whose pages are public: the site runs Data, the
33
+ * page is a published template whose visibility lets it be indexed, and the
34
+ * dataset is still shared with the site. Exactly what the resolver serves,
35
+ * minus the pages a search engine must not be handed.
36
+ */ async function listedTemplates(hostId) {
37
+ const bindings = await readSiteRecordPageBindings(hostId);
38
+ if (!bindings.length) return [];
39
+ const firestore = firebaseAdmin.app().firestore();
40
+ const hostRef = firestore.collection('hosts').doc(hostId);
41
+ const host = (await hostRef.get()).data();
42
+ const orgId = typeof (host == null ? void 0 : host['orgId']) === 'string' ? host['orgId'] : '';
43
+ const org = orgId ? (await firestore.collection('orgs').doc(orgId).get()).data() : undefined;
44
+ if (!isHostPluginEnabled(org, host, BUNDLE_ID)) return [];
45
+ const listed = await Promise.all(bindings.map(async (binding)=>{
46
+ const screen = (await hostRef.collection('screens').doc(binding.screenId).get()).data();
47
+ if (!screen || screen['deletedAt'] || screen['kind'] !== SCREEN_KIND_TEMPLATE || !screen['versionId'] || !isScreenIndexable(screen)) {
48
+ return null;
49
+ }
50
+ const dataset = await readSiteDataset(hostId, binding.datasetId);
51
+ return dataset ? {
52
+ binding,
53
+ dataset
54
+ } : null;
55
+ }));
56
+ return listed.filter((entry)=>entry != null);
57
+ }
58
+ const recordCount = async (dataset)=>(await dataset.ref.collection('records').count().get()).data().count;
59
+ /**
60
+ * Record pages in the sitemap (AGL-3475): one child per record template, sized
61
+ * by its dataset's record count and paged by record id, so a page number names
62
+ * the same records on every fetch. A record with no address has no page and
63
+ * is left out of the file that would have held it.
64
+ */ export const recordPagesSitemapReader = {
65
+ async children ({ hostId }) {
66
+ const templates = await listedTemplates(hostId);
67
+ return Promise.all(templates.map(async ({ binding, dataset })=>({
68
+ key: recordPagesSitemapKey(binding.base),
69
+ urls: await recordCount(dataset)
70
+ })));
71
+ },
72
+ async urls ({ hostId, key, page, perPage }) {
73
+ const template = (await listedTemplates(hostId)).find(({ binding })=>recordPagesSitemapKey(binding.base) === key);
74
+ if (!template) return [];
75
+ const { binding, dataset } = template;
76
+ const rows = await dataset.ref.collection('records').orderBy(FieldPath.documentId()).offset((Math.max(1, page) - 1) * perPage).limit(perPage).select(`values.${binding.slugField}`, 'updatedAt', 'createdAt').get();
77
+ const urls = [];
78
+ for (const row of rows.docs){
79
+ var _row_get;
80
+ const address = recordAddressOf(row.get('values'), binding.slugField);
81
+ if (!address) continue;
82
+ urls.push({
83
+ path: recordPagePath(binding.base, address),
84
+ lastmod: (_row_get = row.get('updatedAt')) != null ? _row_get : row.get('createdAt')
85
+ });
86
+ }
87
+ return urls;
88
+ },
89
+ async listings ({ hostId }) {
90
+ const templates = await listedTemplates(hostId);
91
+ return Promise.all(templates.map(async ({ binding, dataset })=>{
92
+ var _binding_base_split_pop;
93
+ return {
94
+ name: dataset.name || humanizeDatasetFieldId((_binding_base_split_pop = binding.base.split('/').pop()) != null ? _binding_base_split_pop : binding.base),
95
+ base: binding.base,
96
+ count: await recordCount(dataset)
97
+ };
98
+ }));
99
+ }
100
+ };
101
+
102
+ //# sourceMappingURL=record-page-sitemap.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-page-sitemap.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n SCREEN_KIND_TEMPLATE,\n isHostPluginEnabled,\n isScreenIndexable,\n} from '@aglyn/aglyn/server'\nimport type {\n PluginSitemapChild,\n PluginSitemapListing,\n PluginSitemapReader,\n PluginSitemapUrl,\n} from '@aglyn/aglyn/plugin-manager/plugin-sitemap-readers'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { humanizeDatasetFieldId } from '../model/dataset-models'\nimport {\n readSiteDataset,\n readSiteRecordPageBindings,\n type SiteDataset,\n} from './record-page-read.server'\nimport {\n type DatasetRecordPageBinding,\n recordAddressOf,\n recordPagePath,\n} from './record-pages'\n\n/** The sitemap family record pages are listed in: `records-{key}`. */\nexport const RECORD_PAGES_SITEMAP_SECTION = 'records'\n\n/**\n * A binding's child key: its base with each `/` as `--`, which a base segment\n * can never hold (`services/residential` → `services--residential`), so the\n * key names exactly one base.\n */\nexport function recordPagesSitemapKey(base: string): string {\n return base.replace(/\\//g, '--')\n}\n\n/** A template whose record pages a crawler may be told about, with its dataset. */\ninterface ListedTemplate {\n binding: DatasetRecordPageBinding\n dataset: SiteDataset\n}\n\n/**\n * The site's record templates whose pages are public: the site runs Data, the\n * page is a published template whose visibility lets it be indexed, and the\n * dataset is still shared with the site. Exactly what the resolver serves,\n * minus the pages a search engine must not be handed.\n */\nasync function listedTemplates(hostId: string): Promise<ListedTemplate[]> {\n const bindings = await readSiteRecordPageBindings(hostId)\n if (!bindings.length) return []\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = (await hostRef.get()).data() as Record<string, any> | undefined\n const orgId = typeof host?.['orgId'] === 'string' ? host['orgId'] : ''\n const org = orgId\n ? ((await firestore.collection('orgs').doc(orgId).get()).data() as\n | Record<string, any>\n | undefined)\n : undefined\n if (!isHostPluginEnabled(org, host, BUNDLE_ID)) return []\n const listed = await Promise.all(\n bindings.map(async (binding): Promise<ListedTemplate | null> => {\n const screen = (\n await hostRef.collection('screens').doc(binding.screenId).get()\n ).data() as Record<string, any> | undefined\n if (\n !screen ||\n screen['deletedAt'] ||\n screen['kind'] !== SCREEN_KIND_TEMPLATE ||\n !screen['versionId'] ||\n !isScreenIndexable(screen as never)\n ) {\n return null\n }\n const dataset = await readSiteDataset(hostId, binding.datasetId)\n return dataset ? { binding, dataset } : null\n }),\n )\n return listed.filter((entry): entry is ListedTemplate => entry != null)\n}\n\nconst recordCount = async (dataset: SiteDataset): Promise<number> =>\n (await dataset.ref.collection('records').count().get()).data().count\n\n/**\n * Record pages in the sitemap (AGL-3475): one child per record template, sized\n * by its dataset's record count and paged by record id, so a page number names\n * the same records on every fetch. A record with no address has no page and\n * is left out of the file that would have held it.\n */\nexport const recordPagesSitemapReader: PluginSitemapReader = {\n async children({ hostId }): Promise<PluginSitemapChild[]> {\n const templates = await listedTemplates(hostId)\n return Promise.all(\n templates.map(async ({ binding, dataset }) => ({\n key: recordPagesSitemapKey(binding.base),\n urls: await recordCount(dataset),\n })),\n )\n },\n\n async urls({ hostId, key, page, perPage }): Promise<PluginSitemapUrl[]> {\n const template = (await listedTemplates(hostId)).find(\n ({ binding }) => recordPagesSitemapKey(binding.base) === key,\n )\n if (!template) return []\n const { binding, dataset } = template\n const rows = await dataset.ref\n .collection('records')\n .orderBy(FieldPath.documentId())\n .offset((Math.max(1, page) - 1) * perPage)\n .limit(perPage)\n .select(`values.${binding.slugField}`, 'updatedAt', 'createdAt')\n .get()\n const urls: PluginSitemapUrl[] = []\n for (const row of rows.docs) {\n const address = recordAddressOf(row.get('values'), binding.slugField)\n if (!address) continue\n urls.push({\n path: recordPagePath(binding.base, address),\n lastmod: row.get('updatedAt') ?? row.get('createdAt'),\n })\n }\n return urls\n },\n\n async listings({ hostId }): Promise<PluginSitemapListing[]> {\n const templates = await listedTemplates(hostId)\n return Promise.all(\n templates.map(async ({ binding, dataset }) => ({\n name:\n dataset.name ||\n humanizeDatasetFieldId(binding.base.split('/').pop() ?? binding.base),\n base: binding.base,\n count: await recordCount(dataset),\n })),\n )\n },\n}\n"],"names":["SCREEN_KIND_TEMPLATE","isHostPluginEnabled","isScreenIndexable","firebaseAdmin","FieldPath","BUNDLE_ID","humanizeDatasetFieldId","readSiteDataset","readSiteRecordPageBindings","recordAddressOf","recordPagePath","RECORD_PAGES_SITEMAP_SECTION","recordPagesSitemapKey","base","replace","listedTemplates","hostId","bindings","length","firestore","app","hostRef","collection","doc","host","get","data","orgId","org","undefined","listed","Promise","all","map","binding","screen","screenId","dataset","datasetId","filter","entry","recordCount","ref","count","recordPagesSitemapReader","children","templates","key","urls","page","perPage","template","find","rows","orderBy","documentId","offset","Math","max","limit","select","slugField","row","docs","address","push","path","lastmod","listings","name","split","pop"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PageRecordAnswer, PageRecordRequest, PageRecordSource } from '@aglyn/aglyn/app-utils/page-record-sources';
18
+ import { type DatasetRecordPageBinding } from './record-pages';
19
+ /** The record-page source's id on the page-record registry. */
20
+ export declare const RECORD_PAGE_SOURCE_ID = "datasetRecordPages";
21
+ /**
22
+ * A site's record templates, live (AGL-3475) — read once per site however
23
+ * many rows of the Pages list or panels of the editor ask, because each asks
24
+ * through this one listener. The rules let any member of the site read them;
25
+ * only the record-pages route writes them.
26
+ */
27
+ export declare function useSiteRecordPageBindings(hostId: string | null | undefined): {
28
+ bindings: DatasetRecordPageBinding[];
29
+ loading: boolean;
30
+ };
31
+ /** The record a page is previewed with, as last picked, and the picker. */
32
+ export declare function useRecordPagePreviewSelection(screenId: string): [string | undefined, (id: string) => void];
33
+ /**
34
+ * The record a record template is drawn for on the canvas (AGL-3475): the
35
+ * picked one, else the first in the dataset's order — the rows a repeat over
36
+ * the dataset would list, read by the same reader, so a preview never draws a
37
+ * record the site may not see. `none` for every page that is no record
38
+ * template of this site.
39
+ */
40
+ export declare function useRecordPagePreview(request: PageRecordRequest): PageRecordAnswer;
41
+ /** Record templates, as the editor's page-record registry asks for them. */
42
+ export declare const RECORD_PAGE_SOURCE: PageRecordSource;