@aglyn/plugins-data 1.0.0-beta.221 → 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,176 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { useFirestore, useFirestoreCollection, useOrgDataScope } from "@aglyn/tenant-feature-instance";
18
+ import { collection, limit, query } from "firebase/firestore";
19
+ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
20
+ import { readDatasetRepeatRows } from "../repeat/dataset-repeat-rows.js";
21
+ import { DATASET_RECORD_PAGES_COLLECTION, RECORD_PAGES_MAX_PER_SITE, parseRecordPageBinding } from "./record-pages.js";
22
+ /** The record-page source's id on the page-record registry. */ export const RECORD_PAGE_SOURCE_ID = 'datasetRecordPages';
23
+ /**
24
+ * A site's record templates, live (AGL-3475) — read once per site however
25
+ * many rows of the Pages list or panels of the editor ask, because each asks
26
+ * through this one listener. The rules let any member of the site read them;
27
+ * only the record-pages route writes them.
28
+ */ export function useSiteRecordPageBindings(hostId) {
29
+ const firestore = useFirestore();
30
+ const { data, status } = useFirestoreCollection(()=>hostId ? query(collection(firestore, 'hosts', hostId, DATASET_RECORD_PAGES_COLLECTION), limit(RECORD_PAGES_MAX_PER_SITE)) : null, [
31
+ firestore,
32
+ hostId
33
+ ], {
34
+ idField: '$id'
35
+ });
36
+ const bindings = useMemo(()=>(data != null ? data : []).map((doc)=>parseRecordPageBinding(doc.$id, doc)).filter((binding)=>binding != null), [
37
+ data
38
+ ]);
39
+ return {
40
+ bindings,
41
+ loading: status === 'loading'
42
+ };
43
+ }
44
+ /**
45
+ * Which record each record template is previewed with, by page — kept for the
46
+ * editor's session, so switching panels or reopening Page Properties keeps
47
+ * the record the author picked.
48
+ */ const previewSelections = new Map();
49
+ const previewListeners = new Set();
50
+ /** The record a page is previewed with, as last picked, and the picker. */ export function useRecordPagePreviewSelection(screenId) {
51
+ const selected = useSyncExternalStore((listener)=>{
52
+ previewListeners.add(listener);
53
+ return ()=>previewListeners.delete(listener);
54
+ }, ()=>previewSelections.get(screenId), ()=>undefined);
55
+ const select = useCallback((id)=>{
56
+ previewSelections.set(screenId, id);
57
+ for (const listener of [
58
+ ...previewListeners
59
+ ])listener();
60
+ }, [
61
+ screenId
62
+ ]);
63
+ return [
64
+ selected,
65
+ select
66
+ ];
67
+ }
68
+ /** A row's label for the picker: the first text value it has, or its id. */ function rowLabel(row, nameField) {
69
+ var _row_$id;
70
+ const named = nameField ? row[nameField] : undefined;
71
+ if (typeof named === 'string' && named.trim()) return named.trim();
72
+ for (const [key, value] of Object.entries(row)){
73
+ if (key !== '$id' && key !== 'url' && typeof value === 'string' && value.trim()) {
74
+ return value.trim();
75
+ }
76
+ }
77
+ return String((_row_$id = row['$id']) != null ? _row_$id : '');
78
+ }
79
+ /**
80
+ * The record a record template is drawn for on the canvas (AGL-3475): the
81
+ * picked one, else the first in the dataset's order — the rows a repeat over
82
+ * the dataset would list, read by the same reader, so a preview never draws a
83
+ * record the site may not see. `none` for every page that is no record
84
+ * template of this site.
85
+ */ export function useRecordPagePreview(request) {
86
+ const { hostId, screenId } = request;
87
+ const { bindings, loading } = useSiteRecordPageBindings(hostId);
88
+ const binding = bindings.find((one)=>one.screenId === screenId);
89
+ const firestore = useFirestore();
90
+ const { scope, ready } = useOrgDataScope({
91
+ hostId
92
+ });
93
+ const [selected, select] = useRecordPagePreviewSelection(screenId);
94
+ const [rows, setRows] = useState(null);
95
+ const datasetId = binding == null ? void 0 : binding.datasetId;
96
+ useEffect(()=>{
97
+ if (!datasetId || !ready || !scope) {
98
+ setRows(null);
99
+ return undefined;
100
+ }
101
+ let active = true;
102
+ readDatasetRepeatRows({
103
+ firestore,
104
+ scope,
105
+ hostId,
106
+ key: datasetId
107
+ }).then((answer)=>{
108
+ if (!active) return;
109
+ setRows(answer.status === 'ready' ? {
110
+ key: datasetId,
111
+ label: answer.label,
112
+ rowsByKey: answer.rowsByKey
113
+ } : null);
114
+ }, (error)=>{
115
+ console.error(error);
116
+ if (active) setRows(null);
117
+ });
118
+ return ()=>{
119
+ active = false;
120
+ };
121
+ }, [
122
+ firestore,
123
+ scope,
124
+ ready,
125
+ hostId,
126
+ datasetId
127
+ ]);
128
+ return useMemo(()=>{
129
+ var _ref, _records_find, _record_$id;
130
+ if (loading) return {
131
+ status: 'loading'
132
+ };
133
+ if (!binding) return {
134
+ status: 'none'
135
+ };
136
+ if (!rows || rows.key !== binding.datasetId) return {
137
+ status: 'loading'
138
+ };
139
+ const dataset = rows.rowsByKey[binding.datasetId];
140
+ const records = (_ref = dataset == null ? void 0 : dataset.records) != null ? _ref : [];
141
+ const record = (_records_find = records.find((row)=>row['$id'] === selected)) != null ? _records_find : records[0];
142
+ if (!record) return {
143
+ status: 'none'
144
+ };
145
+ return _extends({
146
+ status: 'ready',
147
+ label: rows.label,
148
+ record
149
+ }, (dataset == null ? void 0 : dataset.model) ? {
150
+ model: dataset.model
151
+ } : {}, {
152
+ datasetsByKey: rows.rowsByKey,
153
+ selectedId: String((_record_$id = record['$id']) != null ? _record_$id : ''),
154
+ choices: records.map((row)=>{
155
+ var _row_$id;
156
+ return {
157
+ id: String((_row_$id = row['$id']) != null ? _row_$id : ''),
158
+ label: rowLabel(row, binding.titleField)
159
+ };
160
+ }),
161
+ select
162
+ });
163
+ }, [
164
+ loading,
165
+ binding,
166
+ rows,
167
+ selected,
168
+ select
169
+ ]);
170
+ }
171
+ /** Record templates, as the editor's page-record registry asks for them. */ export const RECORD_PAGE_SOURCE = {
172
+ id: RECORD_PAGE_SOURCE_ID,
173
+ usePageRecord: useRecordPagePreview
174
+ };
175
+
176
+ //# sourceMappingURL=record-page-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-page-source.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 {\n PageRecordAnswer,\n PageRecordRequest,\n PageRecordSource,\n} from '@aglyn/aglyn/app-utils/page-record-sources'\nimport type { RepeatableDataset } from '@aglyn/aglyn/app-utils/expand-repeatables'\nimport {\n useFirestore,\n useFirestoreCollection,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport { collection, limit, query } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport { readDatasetRepeatRows } from '../repeat/dataset-repeat-rows'\nimport {\n DATASET_RECORD_PAGES_COLLECTION,\n type DatasetRecordPageBinding,\n RECORD_PAGES_MAX_PER_SITE,\n parseRecordPageBinding,\n} from './record-pages'\n\n/** The record-page source's id on the page-record registry. */\nexport const RECORD_PAGE_SOURCE_ID = 'datasetRecordPages'\n\n/**\n * A site's record templates, live (AGL-3475) — read once per site however\n * many rows of the Pages list or panels of the editor ask, because each asks\n * through this one listener. The rules let any member of the site read them;\n * only the record-pages route writes them.\n */\nexport function useSiteRecordPageBindings(hostId: string | null | undefined): {\n bindings: DatasetRecordPageBinding[]\n loading: boolean\n} {\n const firestore = useFirestore()\n const { data, status } = useFirestoreCollection<Record<string, unknown> & { $id: string }>(\n () =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, DATASET_RECORD_PAGES_COLLECTION),\n limit(RECORD_PAGES_MAX_PER_SITE),\n )\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n const bindings = useMemo(\n () =>\n (data ?? [])\n .map((doc) => parseRecordPageBinding(doc.$id, doc))\n .filter((binding): binding is DatasetRecordPageBinding => binding != null),\n [data],\n )\n return { bindings, loading: status === 'loading' }\n}\n\n/**\n * Which record each record template is previewed with, by page — kept for the\n * editor's session, so switching panels or reopening Page Properties keeps\n * the record the author picked.\n */\nconst previewSelections = new Map<string, string>()\nconst previewListeners = new Set<() => void>()\n\n/** The record a page is previewed with, as last picked, and the picker. */\nexport function useRecordPagePreviewSelection(\n screenId: string,\n): [string | undefined, (id: string) => void] {\n const selected = useSyncExternalStore(\n (listener) => {\n previewListeners.add(listener)\n return () => previewListeners.delete(listener)\n },\n () => previewSelections.get(screenId),\n () => undefined,\n )\n const select = useCallback(\n (id: string) => {\n previewSelections.set(screenId, id)\n for (const listener of [...previewListeners]) listener()\n },\n [screenId],\n )\n return [selected, select]\n}\n\n/** A row's label for the picker: the first text value it has, or its id. */\nfunction rowLabel(row: Record<string, unknown>, nameField: string | undefined): string {\n const named = nameField ? row[nameField] : undefined\n if (typeof named === 'string' && named.trim()) return named.trim()\n for (const [key, value] of Object.entries(row)) {\n if (key !== '$id' && key !== 'url' && typeof value === 'string' && value.trim()) {\n return value.trim()\n }\n }\n return String(row['$id'] ?? '')\n}\n\n/**\n * The record a record template is drawn for on the canvas (AGL-3475): the\n * picked one, else the first in the dataset's order — the rows a repeat over\n * the dataset would list, read by the same reader, so a preview never draws a\n * record the site may not see. `none` for every page that is no record\n * template of this site.\n */\nexport function useRecordPagePreview(request: PageRecordRequest): PageRecordAnswer {\n const { hostId, screenId } = request\n const { bindings, loading } = useSiteRecordPageBindings(hostId)\n const binding = bindings.find((one) => one.screenId === screenId)\n const firestore = useFirestore()\n const { scope, ready } = useOrgDataScope({ hostId })\n const [selected, select] = useRecordPagePreviewSelection(screenId)\n const [rows, setRows] = useState<{\n key: string\n label: string\n rowsByKey: Record<string, RepeatableDataset>\n } | null>(null)\n const datasetId = binding?.datasetId\n\n useEffect(() => {\n if (!datasetId || !ready || !scope) {\n setRows(null)\n return undefined\n }\n let active = true\n readDatasetRepeatRows({ firestore, scope, hostId, key: datasetId }).then(\n (answer) => {\n if (!active) return\n setRows(\n answer.status === 'ready'\n ? { key: datasetId, label: answer.label, rowsByKey: answer.rowsByKey }\n : null,\n )\n },\n (error) => {\n console.error(error)\n if (active) setRows(null)\n },\n )\n return () => {\n active = false\n }\n }, [firestore, scope, ready, hostId, datasetId])\n\n return useMemo((): PageRecordAnswer => {\n if (loading) return { status: 'loading' }\n if (!binding) return { status: 'none' }\n if (!rows || rows.key !== binding.datasetId) return { status: 'loading' }\n const dataset = rows.rowsByKey[binding.datasetId]\n const records = dataset?.records ?? []\n const record = records.find((row) => row['$id'] === selected) ?? records[0]\n if (!record) return { status: 'none' }\n return {\n status: 'ready',\n label: rows.label,\n record,\n ...(dataset?.model ? { model: dataset.model } : {}),\n datasetsByKey: rows.rowsByKey,\n selectedId: String(record['$id'] ?? ''),\n choices: records.map((row) => ({\n id: String(row['$id'] ?? ''),\n label: rowLabel(row, binding.titleField),\n })),\n select,\n }\n }, [loading, binding, rows, selected, select])\n}\n\n/** Record templates, as the editor's page-record registry asks for them. */\nexport const RECORD_PAGE_SOURCE: PageRecordSource = {\n id: RECORD_PAGE_SOURCE_ID,\n usePageRecord: useRecordPagePreview,\n}\n"],"names":["useFirestore","useFirestoreCollection","useOrgDataScope","collection","limit","query","useCallback","useEffect","useMemo","useState","useSyncExternalStore","readDatasetRepeatRows","DATASET_RECORD_PAGES_COLLECTION","RECORD_PAGES_MAX_PER_SITE","parseRecordPageBinding","RECORD_PAGE_SOURCE_ID","useSiteRecordPageBindings","hostId","firestore","data","status","idField","bindings","map","doc","$id","filter","binding","loading","previewSelections","Map","previewListeners","Set","useRecordPagePreviewSelection","screenId","selected","listener","add","delete","get","undefined","select","id","set","rowLabel","row","nameField","named","trim","key","value","Object","entries","String","useRecordPagePreview","request","find","one","scope","ready","rows","setRows","datasetId","active","then","answer","label","rowsByKey","error","console","records","record","dataset","model","datasetsByKey","selectedId","choices","titleField","RECORD_PAGE_SOURCE","usePageRecord"],"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 PluginWebApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * A site's record templates (AGL-3475), saved and removed — the only writer
20
+ * of `hosts/{hostId}/recordPages`, which the rules close to every client.
21
+ *
22
+ * A binding decides which page serves `/{base}/…` for every record of a
23
+ * dataset, so it is a routing decision and needs the PUBLISH role on the
24
+ * site, as a redirect does. Saving checks what a published page would
25
+ * otherwise get wrong with nothing red:
26
+ *
27
+ * - the page is a template (`kind: 'template'`), so its own address cannot
28
+ * serve raw `{{item.*}}` — the console converts it first, through the
29
+ * platform's own route, which is the only writer of `kind`;
30
+ * - the dataset is shared with this site, and its address field is a page
31
+ * address;
32
+ * - the plan includes datasets (Starter and up), and the data store is
33
+ * released for the organization;
34
+ * - the base is free: no reserved address, no other plugin's route, no
35
+ * content collection's slug, no other template's base; and the dataset has
36
+ * no other template on this site, so `{{item.url}}` has one answer.
37
+ *
38
+ * Every save and removal drops the record pages it changes — the ones it
39
+ * stops serving and the ones it starts to, which a crawler may have been told
40
+ * were missing.
41
+ */
42
+ export declare const recordPagesHandler: PluginWebApiHandler;
@@ -0,0 +1,348 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { SCREEN_KIND_TEMPLATE, checkEntitlement, hostCollectionKind, hostRoleCanPublish, pluginRequestFromWeb } from "@aglyn/aglyn/server";
19
+ import { listPluginSitemapSections } from "@aglyn/aglyn/plugin-manager/plugin-sitemap-sections";
20
+ import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
21
+ import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession, isServerReleaseFlagOnForOrg, lockdownRefusal } from "@aglyn/tenant-data-admin";
22
+ import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
23
+ import { Timestamp } from "firebase-admin/firestore";
24
+ import { announceDatasetRecords } from "../server/announce-dataset-records.js";
25
+ import { readRecordPageBindingsUncached, recordPagePathsOf } from "./record-page-live-paths.server.js";
26
+ import { readSiteDataset } from "./record-page-read.server.js";
27
+ import { RECORD_PAGES_MAX_PER_SITE, isRecordAddressField, normalizeRecordPageBase, recordPageBaseRefusal } from "./record-pages.js";
28
+ /** The optional fields a binding may name, each one a field of the dataset. */ const OPTIONAL_FIELDS = [
29
+ 'titleField',
30
+ 'seoTitleField',
31
+ 'seoDescriptionField',
32
+ 'seoImageField'
33
+ ];
34
+ const text = (value)=>typeof value === 'string' ? value.trim() : '';
35
+ /**
36
+ * The first segment of every route another plugin serves pages under on every
37
+ * site, read off the sections they declare for the sitemap — `/products/{slug}`
38
+ * is `products` — so a base cannot shadow a store without this plugin naming
39
+ * one.
40
+ */ function pluginRouteSegments() {
41
+ return listPluginSitemapSections().map((section)=>{
42
+ var _section_path_split_filter_;
43
+ return (_section_path_split_filter_ = section.path.split('/').filter(Boolean)[0]) != null ? _section_path_split_filter_ : '';
44
+ }).filter(Boolean);
45
+ }
46
+ /**
47
+ * A site's record templates (AGL-3475), saved and removed — the only writer
48
+ * of `hosts/{hostId}/recordPages`, which the rules close to every client.
49
+ *
50
+ * A binding decides which page serves `/{base}/…` for every record of a
51
+ * dataset, so it is a routing decision and needs the PUBLISH role on the
52
+ * site, as a redirect does. Saving checks what a published page would
53
+ * otherwise get wrong with nothing red:
54
+ *
55
+ * - the page is a template (`kind: 'template'`), so its own address cannot
56
+ * serve raw `{{item.*}}` — the console converts it first, through the
57
+ * platform's own route, which is the only writer of `kind`;
58
+ * - the dataset is shared with this site, and its address field is a page
59
+ * address;
60
+ * - the plan includes datasets (Starter and up), and the data store is
61
+ * released for the organization;
62
+ * - the base is free: no reserved address, no other plugin's route, no
63
+ * content collection's slug, no other template's base; and the dataset has
64
+ * no other template on this site, so `{{item.url}}` has one answer.
65
+ *
66
+ * Every save and removal drops the record pages it changes — the ones it
67
+ * stops serving and the ones it starts to, which a crawler may have been told
68
+ * were missing.
69
+ */ export const recordPagesHandler = async (request)=>{
70
+ var _headers_authorization;
71
+ const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request);
72
+ const headers = rawHeaders;
73
+ if (method !== 'POST') {
74
+ return Response.json({
75
+ error: 'Method not allowed'
76
+ }, {
77
+ status: 405
78
+ });
79
+ }
80
+ const authorization = (_headers_authorization = headers.authorization) != null ? _headers_authorization : '';
81
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
82
+ if (!idToken) return Response.json({
83
+ error: 'Unauthenticated'
84
+ }, {
85
+ status: 401
86
+ });
87
+ const action = text(body == null ? void 0 : body.action);
88
+ const hostId = text(body == null ? void 0 : body.hostId);
89
+ const screenId = text(body == null ? void 0 : body.screenId);
90
+ if (action !== 'save' && action !== 'remove') {
91
+ return Response.json({
92
+ error: 'Unknown action'
93
+ }, {
94
+ status: 400
95
+ });
96
+ }
97
+ if (!hostId || !screenId || hostId.includes('/') || screenId.includes('/')) {
98
+ return Response.json({
99
+ error: 'Missing hostId or screenId'
100
+ }, {
101
+ status: 400
102
+ });
103
+ }
104
+ try {
105
+ var _bindings_find;
106
+ var _host_memberRoles;
107
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
108
+ if (!decoded.email_verified && !isImpersonationSession(decoded)) {
109
+ return emailUnverifiedResponse();
110
+ }
111
+ const staff = decoded['staff'] === true;
112
+ const firestore = firebaseAdmin.app().firestore();
113
+ const hostRef = firestore.collection('hosts').doc(hostId);
114
+ const hostSnapshot = await hostRef.get();
115
+ if (!hostSnapshot.exists) {
116
+ return Response.json({
117
+ error: 'Unknown site'
118
+ }, {
119
+ status: 404
120
+ });
121
+ }
122
+ const host = hostSnapshot.data();
123
+ if (!staff && !hostRoleCanPublish((_host_memberRoles = host['memberRoles']) == null ? void 0 : _host_memberRoles[decoded.uid])) {
124
+ return Response.json({
125
+ error: 'Record templates are published routes — this needs the publisher role on the site'
126
+ }, {
127
+ status: 403
128
+ });
129
+ }
130
+ const orgId = text(host['orgId']);
131
+ const org = orgId ? (await firestore.collection('orgs').doc(orgId).get()).data() : undefined;
132
+ const locked = await lockdownRefusal({
133
+ request,
134
+ staff,
135
+ uid: decoded.uid,
136
+ org: org,
137
+ host: host
138
+ });
139
+ if (locked) return locked;
140
+ if (!staff && orgId && !await isServerReleaseFlagOnForOrg('release_data_store', orgId)) {
141
+ return Response.json({
142
+ error: 'Not available'
143
+ }, {
144
+ status: 404
145
+ });
146
+ }
147
+ // Spelled out, not DATASET_RECORD_PAGES_COLLECTION: the media-corpus guard
148
+ // (host-content-media-coverage.spec) finds host collections by literal.
149
+ const bindingsRef = hostRef.collection('recordPages');
150
+ const bindings = await readRecordPageBindingsUncached(hostId);
151
+ const current = (_bindings_find = bindings.find((binding)=>binding.screenId === screenId)) != null ? _bindings_find : null;
152
+ if (action === 'remove') {
153
+ if (!current) return Response.json({
154
+ ok: true,
155
+ removed: false
156
+ }, {
157
+ status: 200
158
+ });
159
+ const stale = await recordPagePathsOf(hostId, current).catch(()=>({
160
+ paths: []
161
+ }));
162
+ await bindingsRef.doc(screenId).delete();
163
+ await dropRecordPages(hostId, current, stale.paths, 'record template removed');
164
+ return Response.json({
165
+ ok: true,
166
+ removed: true
167
+ }, {
168
+ status: 200
169
+ });
170
+ }
171
+ if (!checkEntitlement(org, 'dataStore')) {
172
+ return Response.json({
173
+ error: 'Record pages need a Starter plan or higher'
174
+ }, {
175
+ status: 403
176
+ });
177
+ }
178
+ const screen = (await hostRef.collection('screens').doc(screenId).get()).data();
179
+ if (!screen || screen['deletedAt']) {
180
+ return Response.json({
181
+ error: 'Unknown page'
182
+ }, {
183
+ status: 404
184
+ });
185
+ }
186
+ if (screen['kind'] !== SCREEN_KIND_TEMPLATE) {
187
+ return Response.json({
188
+ error: 'Make this page a template first',
189
+ code: 'not-template'
190
+ }, {
191
+ status: 409
192
+ });
193
+ }
194
+ const datasetId = text(body == null ? void 0 : body.datasetId);
195
+ const dataset = await readSiteDataset(hostId, datasetId);
196
+ if (!dataset) {
197
+ return Response.json({
198
+ error: 'That dataset is not shared with this site'
199
+ }, {
200
+ status: 404
201
+ });
202
+ }
203
+ const slugField = text(body == null ? void 0 : body.slugField);
204
+ if (!isRecordAddressField(dataset.model.fields[slugField])) {
205
+ return Response.json({
206
+ error: 'Pick a Page address field of the dataset for the record pages’ addresses'
207
+ }, {
208
+ status: 400
209
+ });
210
+ }
211
+ const optional = {};
212
+ for (const key of OPTIONAL_FIELDS){
213
+ const fieldId = text(body == null ? void 0 : body[key]);
214
+ if (!fieldId) continue;
215
+ if (!dataset.model.fields[fieldId]) {
216
+ return Response.json({
217
+ error: `The dataset has no field "${fieldId}"`
218
+ }, {
219
+ status: 400
220
+ });
221
+ }
222
+ optional[key] = fieldId;
223
+ }
224
+ const others = bindings.filter((binding)=>binding.screenId !== screenId);
225
+ if (!current && bindings.length >= RECORD_PAGES_MAX_PER_SITE) {
226
+ return Response.json({
227
+ error: `A site can have up to ${RECORD_PAGES_MAX_PER_SITE} record templates`
228
+ }, {
229
+ status: 403
230
+ });
231
+ }
232
+ if (others.some((binding)=>binding.datasetId === dataset.id)) {
233
+ return Response.json({
234
+ error: `${dataset.name || 'This dataset'} already has a record template on this site`
235
+ }, {
236
+ status: 409
237
+ });
238
+ }
239
+ const collections = await hostRef.collection('collections').select('slug', 'kind').get();
240
+ const refusal = recordPageBaseRefusal(body == null ? void 0 : body.base, {
241
+ collectionSlugs: collections.docs.filter((doc)=>hostCollectionKind(doc.data()) === 'content').map((doc)=>text(doc.get('slug'))).filter(Boolean),
242
+ pluginRouteSegments: pluginRouteSegments(),
243
+ otherBases: others.map((binding)=>binding.base)
244
+ });
245
+ if (refusal) return Response.json({
246
+ error: refusal
247
+ }, {
248
+ status: 400
249
+ });
250
+ const base = normalizeRecordPageBase(body == null ? void 0 : body.base);
251
+ const binding = _extends({
252
+ screenId,
253
+ datasetId: dataset.id,
254
+ base,
255
+ slugField
256
+ }, optional);
257
+ const { screenId: _id } = binding, stored = _object_without_properties_loose(binding, [
258
+ "screenId"
259
+ ]);
260
+ await bindingsRef.doc(screenId).set(_extends({}, stored, {
261
+ updatedAt: Timestamp.now(),
262
+ updatedBy: decoded.uid
263
+ }));
264
+ const [before, after] = await Promise.all([
265
+ current ? recordPagePathsOf(hostId, current).catch(()=>({
266
+ paths: []
267
+ })) : {
268
+ paths: []
269
+ },
270
+ recordPagePathsOf(hostId, binding).catch(()=>({
271
+ paths: []
272
+ }))
273
+ ]);
274
+ await dropRecordPages(hostId, binding, [
275
+ ...after.paths,
276
+ ...current ? [
277
+ `/${current.base}`
278
+ ] : [],
279
+ ...before.paths
280
+ ], 'record template saved');
281
+ return Response.json({
282
+ ok: true,
283
+ binding
284
+ }, {
285
+ status: 200
286
+ });
287
+ } catch (error) {
288
+ if (isRefusedIdToken(error)) {
289
+ return Response.json({
290
+ error: 'Unauthenticated'
291
+ }, {
292
+ status: 401
293
+ });
294
+ }
295
+ console.error(error);
296
+ return Response.json({
297
+ error: 'Record template operation failed'
298
+ }, {
299
+ status: 500
300
+ });
301
+ }
302
+ };
303
+ /**
304
+ * Drops what a binding change makes stale. Best effort: the binding is
305
+ * written, and the host's data tag is underneath.
306
+ *
307
+ * - The record pages it starts or stops serving, and its base, which a site
308
+ * often publishes as the listing — and which is always one path, so the
309
+ * drop always reaches the tenant and busts the data tag the bindings are
310
+ * cached under. A drop naming no path is skipped outright.
311
+ * - Every page that repeats over the dataset, on every site sharing it: its
312
+ * rows have gained or lost their `{{item.url}}`.
313
+ */ async function dropRecordPages(hostId, binding, paths, reason) {
314
+ try {
315
+ await dropPluginSiteCache({
316
+ hostIds: [
317
+ hostId
318
+ ],
319
+ paths: {
320
+ [hostId]: [
321
+ ...new Set([
322
+ `/${binding.base}`,
323
+ ...paths
324
+ ])
325
+ ]
326
+ },
327
+ reason
328
+ });
329
+ } catch (error) {
330
+ console.error('[record-pages] cache drop failed', hostId, error);
331
+ }
332
+ try {
333
+ var _dataset_ref_parent_parent;
334
+ const dataset = await readSiteDataset(hostId, binding.datasetId);
335
+ const orgId = dataset == null ? void 0 : (_dataset_ref_parent_parent = dataset.ref.parent.parent) == null ? void 0 : _dataset_ref_parent_parent.id;
336
+ if (dataset && orgId) {
337
+ await announceDatasetRecords({
338
+ firestore: firebaseAdmin.app().firestore(),
339
+ orgId,
340
+ datasetId: dataset.id
341
+ });
342
+ }
343
+ } catch (error) {
344
+ console.error('[record-pages] dataset announce failed', hostId, error);
345
+ }
346
+ }
347
+
348
+ //# sourceMappingURL=record-pages-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-pages-route.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 checkEntitlement,\n hostCollectionKind,\n hostRoleCanPublish,\n pluginRequestFromWeb,\n type PluginWebApiHandler,\n} from '@aglyn/aglyn/server'\nimport { listPluginSitemapSections } from '@aglyn/aglyn/plugin-manager/plugin-sitemap-sections'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { Timestamp } from 'firebase-admin/firestore'\nimport { announceDatasetRecords } from '../server/announce-dataset-records'\nimport { readRecordPageBindingsUncached, recordPagePathsOf } from './record-page-live-paths.server'\nimport { readSiteDataset } from './record-page-read.server'\nimport {\n type DatasetRecordPageBinding,\n RECORD_PAGES_MAX_PER_SITE,\n isRecordAddressField,\n normalizeRecordPageBase,\n recordPageBaseRefusal,\n} from './record-pages'\n\n/** The optional fields a binding may name, each one a field of the dataset. */\nconst OPTIONAL_FIELDS = [\n 'titleField',\n 'seoTitleField',\n 'seoDescriptionField',\n 'seoImageField',\n] as const\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/**\n * The first segment of every route another plugin serves pages under on every\n * site, read off the sections they declare for the sitemap — `/products/{slug}`\n * is `products` — so a base cannot shadow a store without this plugin naming\n * one.\n */\nfunction pluginRouteSegments(): string[] {\n return listPluginSitemapSections()\n .map((section) => section.path.split('/').filter(Boolean)[0] ?? '')\n .filter(Boolean)\n}\n\n/**\n * A site's record templates (AGL-3475), saved and removed — the only writer\n * of `hosts/{hostId}/recordPages`, which the rules close to every client.\n *\n * A binding decides which page serves `/{base}/…` for every record of a\n * dataset, so it is a routing decision and needs the PUBLISH role on the\n * site, as a redirect does. Saving checks what a published page would\n * otherwise get wrong with nothing red:\n *\n * - the page is a template (`kind: 'template'`), so its own address cannot\n * serve raw `{{item.*}}` — the console converts it first, through the\n * platform's own route, which is the only writer of `kind`;\n * - the dataset is shared with this site, and its address field is a page\n * address;\n * - the plan includes datasets (Starter and up), and the data store is\n * released for the organization;\n * - the base is free: no reserved address, no other plugin's route, no\n * content collection's slug, no other template's base; and the dataset has\n * no other template on this site, so `{{item.url}}` has one answer.\n *\n * Every save and removal drops the record pages it changes — the ones it\n * stops serving and the ones it starts to, which a crawler may have been told\n * were missing.\n */\nexport const recordPagesHandler: PluginWebApiHandler = async (request) => {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n const action = text(body?.action)\n const hostId = text(body?.hostId)\n const screenId = text(body?.screenId)\n if (action !== 'save' && action !== 'remove') {\n return Response.json({ error: 'Unknown action' }, { status: 400 })\n }\n if (!hostId || !screenId || hostId.includes('/') || screenId.includes('/')) {\n return Response.json({ error: 'Missing hostId or screenId' }, { status: 400 })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return Response.json({ error: 'Unknown site' }, { status: 404 })\n }\n const host = hostSnapshot.data() as Record<string, unknown>\n if (!staff && !hostRoleCanPublish((host['memberRoles'] as Record<string, unknown>)?.[decoded.uid])) {\n return Response.json(\n { error: 'Record templates are published routes — this needs the publisher role on the site' },\n { status: 403 },\n )\n }\n const orgId = text(host['orgId'])\n const org = orgId\n ? ((await firestore.collection('orgs').doc(orgId).get()).data() as Record<string, unknown> | undefined)\n : undefined\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: org as never,\n host: host as never,\n })\n if (locked) return locked\n if (!staff && orgId && !(await isServerReleaseFlagOnForOrg('release_data_store', orgId))) {\n return Response.json({ error: 'Not available' }, { status: 404 })\n }\n\n // Spelled out, not DATASET_RECORD_PAGES_COLLECTION: the media-corpus guard\n // (host-content-media-coverage.spec) finds host collections by literal.\n const bindingsRef = hostRef.collection('recordPages')\n const bindings = await readRecordPageBindingsUncached(hostId)\n const current = bindings.find((binding) => binding.screenId === screenId) ?? null\n\n if (action === 'remove') {\n if (!current) return Response.json({ ok: true, removed: false }, { status: 200 })\n const stale = await recordPagePathsOf(hostId, current).catch(() => ({ paths: [] as string[] }))\n await bindingsRef.doc(screenId).delete()\n await dropRecordPages(hostId, current, stale.paths, 'record template removed')\n return Response.json({ ok: true, removed: true }, { status: 200 })\n }\n\n if (!checkEntitlement(org as never, 'dataStore')) {\n return Response.json(\n { error: 'Record pages need a Starter plan or higher' },\n { status: 403 },\n )\n }\n const screen = (await hostRef.collection('screens').doc(screenId).get()).data() as\n | Record<string, unknown>\n | undefined\n if (!screen || screen['deletedAt']) {\n return Response.json({ error: 'Unknown page' }, { status: 404 })\n }\n if (screen['kind'] !== SCREEN_KIND_TEMPLATE) {\n return Response.json(\n { error: 'Make this page a template first', code: 'not-template' },\n { status: 409 },\n )\n }\n\n const datasetId = text(body?.datasetId)\n const dataset = await readSiteDataset(hostId, datasetId)\n if (!dataset) {\n return Response.json({ error: 'That dataset is not shared with this site' }, { status: 404 })\n }\n const slugField = text(body?.slugField)\n if (!isRecordAddressField(dataset.model.fields[slugField])) {\n return Response.json(\n { error: 'Pick a Page address field of the dataset for the record pages’ addresses' },\n { status: 400 },\n )\n }\n const optional: Partial<DatasetRecordPageBinding> = {}\n for (const key of OPTIONAL_FIELDS) {\n const fieldId = text(body?.[key])\n if (!fieldId) continue\n if (!dataset.model.fields[fieldId]) {\n return Response.json({ error: `The dataset has no field \"${fieldId}\"` }, { status: 400 })\n }\n optional[key] = fieldId\n }\n\n const others = bindings.filter((binding) => binding.screenId !== screenId)\n if (!current && bindings.length >= RECORD_PAGES_MAX_PER_SITE) {\n return Response.json(\n { error: `A site can have up to ${RECORD_PAGES_MAX_PER_SITE} record templates` },\n { status: 403 },\n )\n }\n if (others.some((binding) => binding.datasetId === dataset.id)) {\n return Response.json(\n { error: `${dataset.name || 'This dataset'} already has a record template on this site` },\n { status: 409 },\n )\n }\n const collections = await hostRef.collection('collections').select('slug', 'kind').get()\n const refusal = recordPageBaseRefusal(body?.base, {\n collectionSlugs: collections.docs\n .filter((doc) => hostCollectionKind(doc.data() as never) === 'content')\n .map((doc) => text(doc.get('slug')))\n .filter(Boolean),\n pluginRouteSegments: pluginRouteSegments(),\n otherBases: others.map((binding) => binding.base),\n })\n if (refusal) return Response.json({ error: refusal }, { status: 400 })\n const base = normalizeRecordPageBase(body?.base) as string\n\n const binding: DatasetRecordPageBinding = {\n screenId,\n datasetId: dataset.id,\n base,\n slugField,\n ...optional,\n }\n const { screenId: _id, ...stored } = binding\n await bindingsRef.doc(screenId).set({\n ...stored,\n updatedAt: Timestamp.now(),\n updatedBy: decoded.uid,\n })\n const [before, after] = await Promise.all([\n current ? recordPagePathsOf(hostId, current).catch(() => ({ paths: [] as string[] })) : { paths: [] },\n recordPagePathsOf(hostId, binding).catch(() => ({ paths: [] as string[] })),\n ])\n await dropRecordPages(\n hostId,\n binding,\n [...after.paths, ...(current ? [`/${current.base}`] : []), ...before.paths],\n 'record template saved',\n )\n return Response.json({ ok: true, binding }, { status: 200 })\n } catch (error) {\n if (isRefusedIdToken(error)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n console.error(error)\n return Response.json({ error: 'Record template operation failed' }, { status: 500 })\n }\n}\n\n/**\n * Drops what a binding change makes stale. Best effort: the binding is\n * written, and the host's data tag is underneath.\n *\n * - The record pages it starts or stops serving, and its base, which a site\n * often publishes as the listing — and which is always one path, so the\n * drop always reaches the tenant and busts the data tag the bindings are\n * cached under. A drop naming no path is skipped outright.\n * - Every page that repeats over the dataset, on every site sharing it: its\n * rows have gained or lost their `{{item.url}}`.\n */\nasync function dropRecordPages(\n hostId: string,\n binding: DatasetRecordPageBinding,\n paths: string[],\n reason: string,\n): Promise<void> {\n try {\n await dropPluginSiteCache({\n hostIds: [hostId],\n paths: { [hostId]: [...new Set([`/${binding.base}`, ...paths])] },\n reason,\n })\n } catch (error) {\n console.error('[record-pages] cache drop failed', hostId, error)\n }\n try {\n const dataset = await readSiteDataset(hostId, binding.datasetId)\n const orgId = dataset?.ref.parent.parent?.id\n if (dataset && orgId) {\n await announceDatasetRecords({\n firestore: firebaseAdmin.app().firestore(),\n orgId,\n datasetId: dataset.id,\n })\n }\n } catch (error) {\n console.error('[record-pages] dataset announce failed', hostId, error)\n }\n}\n"],"names":["SCREEN_KIND_TEMPLATE","checkEntitlement","hostCollectionKind","hostRoleCanPublish","pluginRequestFromWeb","listPluginSitemapSections","dropPluginSiteCache","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","isRefusedIdToken","Timestamp","announceDatasetRecords","readRecordPageBindingsUncached","recordPagePathsOf","readSiteDataset","RECORD_PAGES_MAX_PER_SITE","isRecordAddressField","normalizeRecordPageBase","recordPageBaseRefusal","OPTIONAL_FIELDS","text","value","trim","pluginRouteSegments","map","section","path","split","filter","Boolean","recordPagesHandler","request","headers","method","body","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","action","hostId","screenId","includes","bindings","host","decoded","app","auth","verifyIdToken","email_verified","staff","firestore","hostRef","collection","doc","hostSnapshot","get","exists","data","uid","orgId","org","locked","bindingsRef","current","find","binding","ok","removed","stale","catch","paths","delete","dropRecordPages","screen","code","datasetId","dataset","slugField","model","fields","optional","key","fieldId","others","some","id","name","collections","select","refusal","base","collectionSlugs","docs","otherBases","_id","stored","set","updatedAt","now","updatedBy","before","after","Promise","all","console","reason","hostIds","Set","ref","parent"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,EACpBC,gBAAgB,EAChBC,kBAAkB,EAClBC,kBAAkB,EAClBC,oBAAoB,QAEf,sBAAqB;AAC5B,SAASC,yBAAyB,QAAQ,sDAAqD;AAC/F,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SACEC,uBAAuB,EACvBC,aAAa,EACbC,sBAAsB,EACtBC,2BAA2B,EAC3BC,eAAe,QACV,2BAA0B;AACjC,SAASC,gBAAgB,QAAQ,mDAAkD;AACnF,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,sBAAsB,QAAQ,wCAAoC;AAC3E,SAASC,8BAA8B,EAAEC,iBAAiB,QAAQ,qCAAiC;AACnG,SAASC,eAAe,QAAQ,+BAA2B;AAC3D,SAEEC,yBAAyB,EACzBC,oBAAoB,EACpBC,uBAAuB,EACvBC,qBAAqB,QAChB,oBAAgB;AAEvB,6EAA6E,GAC7E,MAAMC,kBAAkB;IACtB;IACA;IACA;IACA;CACD;AAED,MAAMC,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AAErF;;;;;CAKC,GACD,SAASC;IACP,OAAOrB,4BACJsB,GAAG,CAAC,CAACC;YAAYA;gBAAAA,8BAAAA,QAAQC,IAAI,CAACC,KAAK,CAAC,KAAKC,MAAM,CAACC,QAAQ,CAAC,EAAE,YAA1CJ,8BAA8C;OAC/DG,MAAM,CAACC;AACZ;AAEA;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,MAAMC,qBAA0C,OAAOC;QAMtCC;IALtB,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEF,SAASG,UAAU,EAAE,GAAG,MAAMlC,qBAAqB8B;IACzE,MAAMC,UAAUG;IAChB,IAAIF,WAAW,QAAQ;QACrB,OAAOG,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAqB,GAAG;YAAEC,QAAQ;QAAI;IACtE;IACA,MAAMC,iBAAgBR,yBAAAA,QAAQQ,aAAa,YAArBR,yBAAyB;IAC/C,MAAMS,UAAUD,cAAcE,UAAU,CAAC,aACrCF,cAAcG,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOL,SAASC,IAAI,CAAC;QAAEC,OAAO;IAAkB,GAAG;QAAEC,QAAQ;IAAI;IAE/E,MAAMO,SAAS1B,KAAKc,wBAAAA,KAAMY,MAAM;IAChC,MAAMC,SAAS3B,KAAKc,wBAAAA,KAAMa,MAAM;IAChC,MAAMC,WAAW5B,KAAKc,wBAAAA,KAAMc,QAAQ;IACpC,IAAIF,WAAW,UAAUA,WAAW,UAAU;QAC5C,OAAOV,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAiB,GAAG;YAAEC,QAAQ;QAAI;IAClE;IACA,IAAI,CAACQ,UAAU,CAACC,YAAYD,OAAOE,QAAQ,CAAC,QAAQD,SAASC,QAAQ,CAAC,MAAM;QAC1E,OAAOb,SAASC,IAAI,CAAC;YAAEC,OAAO;QAA6B,GAAG;YAAEC,QAAQ;QAAI;IAC9E;IAEA,IAAI;YAuCcW;YA1BmBC;QAZnC,MAAMC,UAAU,MAAM/C,cAAcgD,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACd;QAC/D,IAAI,CAACW,QAAQI,cAAc,IAAI,CAAClD,uBAAuB8C,UAAU;YAC/D,OAAOhD;QACT;QACA,MAAMqD,QAAQL,OAAO,CAAC,QAAQ,KAAK;QACnC,MAAMM,YAAYrD,cAAcgD,GAAG,GAAGK,SAAS;QAC/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACd;QAClD,MAAMe,eAAe,MAAMH,QAAQI,GAAG;QACtC,IAAI,CAACD,aAAaE,MAAM,EAAE;YACxB,OAAO5B,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QACA,MAAMY,OAAOW,aAAaG,IAAI;QAC9B,IAAI,CAACR,SAAS,CAACzD,oBAAoBmD,oBAAAA,IAAI,CAAC,cAAc,qBAApB,AAACA,iBAAiD,CAACC,QAAQc,GAAG,CAAC,GAAG;YAClG,OAAO9B,SAASC,IAAI,CAClB;gBAAEC,OAAO;YAAoF,GAC7F;gBAAEC,QAAQ;YAAI;QAElB;QACA,MAAM4B,QAAQ/C,KAAK+B,IAAI,CAAC,QAAQ;QAChC,MAAMiB,MAAMD,QACP,AAAC,CAAA,MAAMT,UAAUE,UAAU,CAAC,QAAQC,GAAG,CAACM,OAAOJ,GAAG,EAAC,EAAGE,IAAI,KAC3DpB;QACJ,MAAMwB,SAAS,MAAM7D,gBAAgB;YACnCuB;YACA0B;YACAS,KAAKd,QAAQc,GAAG;YAChBE,KAAKA;YACLjB,MAAMA;QACR;QACA,IAAIkB,QAAQ,OAAOA;QACnB,IAAI,CAACZ,SAASU,SAAS,CAAE,MAAM5D,4BAA4B,sBAAsB4D,QAAS;YACxF,OAAO/B,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAgB,GAAG;gBAAEC,QAAQ;YAAI;QACjE;QAEA,2EAA2E;QAC3E,wEAAwE;QACxE,MAAM+B,cAAcX,QAAQC,UAAU,CAAC;QACvC,MAAMV,WAAW,MAAMtC,+BAA+BmC;QACtD,MAAMwB,WAAUrB,iBAAAA,SAASsB,IAAI,CAAC,CAACC,UAAYA,QAAQzB,QAAQ,KAAKA,qBAAhDE,iBAA6D;QAE7E,IAAIJ,WAAW,UAAU;YACvB,IAAI,CAACyB,SAAS,OAAOnC,SAASC,IAAI,CAAC;gBAAEqC,IAAI;gBAAMC,SAAS;YAAM,GAAG;gBAAEpC,QAAQ;YAAI;YAC/E,MAAMqC,QAAQ,MAAM/D,kBAAkBkC,QAAQwB,SAASM,KAAK,CAAC,IAAO,CAAA;oBAAEC,OAAO,EAAE;gBAAa,CAAA;YAC5F,MAAMR,YAAYT,GAAG,CAACb,UAAU+B,MAAM;YACtC,MAAMC,gBAAgBjC,QAAQwB,SAASK,MAAME,KAAK,EAAE;YACpD,OAAO1C,SAASC,IAAI,CAAC;gBAAEqC,IAAI;gBAAMC,SAAS;YAAK,GAAG;gBAAEpC,QAAQ;YAAI;QAClE;QAEA,IAAI,CAACzC,iBAAiBsE,KAAc,cAAc;YAChD,OAAOhC,SAASC,IAAI,CAClB;gBAAEC,OAAO;YAA6C,GACtD;gBAAEC,QAAQ;YAAI;QAElB;QACA,MAAM0C,SAAS,AAAC,CAAA,MAAMtB,QAAQC,UAAU,CAAC,WAAWC,GAAG,CAACb,UAAUe,GAAG,EAAC,EAAGE,IAAI;QAG7E,IAAI,CAACgB,UAAUA,MAAM,CAAC,YAAY,EAAE;YAClC,OAAO7C,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QACA,IAAI0C,MAAM,CAAC,OAAO,KAAKpF,sBAAsB;YAC3C,OAAOuC,SAASC,IAAI,CAClB;gBAAEC,OAAO;gBAAmC4C,MAAM;YAAe,GACjE;gBAAE3C,QAAQ;YAAI;QAElB;QAEA,MAAM4C,YAAY/D,KAAKc,wBAAAA,KAAMiD,SAAS;QACtC,MAAMC,UAAU,MAAMtE,gBAAgBiC,QAAQoC;QAC9C,IAAI,CAACC,SAAS;YACZ,OAAOhD,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAA4C,GAAG;gBAAEC,QAAQ;YAAI;QAC7F;QACA,MAAM8C,YAAYjE,KAAKc,wBAAAA,KAAMmD,SAAS;QACtC,IAAI,CAACrE,qBAAqBoE,QAAQE,KAAK,CAACC,MAAM,CAACF,UAAU,GAAG;YAC1D,OAAOjD,SAASC,IAAI,CAClB;gBAAEC,OAAO;YAA2E,GACpF;gBAAEC,QAAQ;YAAI;QAElB;QACA,MAAMiD,WAA8C,CAAC;QACrD,KAAK,MAAMC,OAAOtE,gBAAiB;YACjC,MAAMuE,UAAUtE,KAAKc,wBAAAA,IAAM,CAACuD,IAAI;YAChC,IAAI,CAACC,SAAS;YACd,IAAI,CAACN,QAAQE,KAAK,CAACC,MAAM,CAACG,QAAQ,EAAE;gBAClC,OAAOtD,SAASC,IAAI,CAAC;oBAAEC,OAAO,CAAC,0BAA0B,EAAEoD,QAAQ,CAAC,CAAC;gBAAC,GAAG;oBAAEnD,QAAQ;gBAAI;YACzF;YACAiD,QAAQ,CAACC,IAAI,GAAGC;QAClB;QAEA,MAAMC,SAASzC,SAAStB,MAAM,CAAC,CAAC6C,UAAYA,QAAQzB,QAAQ,KAAKA;QACjE,IAAI,CAACuB,WAAWrB,SAASN,MAAM,IAAI7B,2BAA2B;YAC5D,OAAOqB,SAASC,IAAI,CAClB;gBAAEC,OAAO,CAAC,sBAAsB,EAAEvB,0BAA0B,iBAAiB,CAAC;YAAC,GAC/E;gBAAEwB,QAAQ;YAAI;QAElB;QACA,IAAIoD,OAAOC,IAAI,CAAC,CAACnB,UAAYA,QAAQU,SAAS,KAAKC,QAAQS,EAAE,GAAG;YAC9D,OAAOzD,SAASC,IAAI,CAClB;gBAAEC,OAAO,GAAG8C,QAAQU,IAAI,IAAI,eAAe,2CAA2C,CAAC;YAAC,GACxF;gBAAEvD,QAAQ;YAAI;QAElB;QACA,MAAMwD,cAAc,MAAMpC,QAAQC,UAAU,CAAC,eAAeoC,MAAM,CAAC,QAAQ,QAAQjC,GAAG;QACtF,MAAMkC,UAAU/E,sBAAsBgB,wBAAAA,KAAMgE,IAAI,EAAE;YAChDC,iBAAiBJ,YAAYK,IAAI,CAC9BxE,MAAM,CAAC,CAACiC,MAAQ9D,mBAAmB8D,IAAII,IAAI,QAAiB,WAC5DzC,GAAG,CAAC,CAACqC,MAAQzC,KAAKyC,IAAIE,GAAG,CAAC,UAC1BnC,MAAM,CAACC;YACVN,qBAAqBA;YACrB8E,YAAYV,OAAOnE,GAAG,CAAC,CAACiD,UAAYA,QAAQyB,IAAI;QAClD;QACA,IAAID,SAAS,OAAO7D,SAASC,IAAI,CAAC;YAAEC,OAAO2D;QAAQ,GAAG;YAAE1D,QAAQ;QAAI;QACpE,MAAM2D,OAAOjF,wBAAwBiB,wBAAAA,KAAMgE,IAAI;QAE/C,MAAMzB,UAAoC;YACxCzB;YACAmC,WAAWC,QAAQS,EAAE;YACrBK;YACAb;WACGG;QAEL,MAAM,EAAExC,UAAUsD,GAAG,EAAa,GAAG7B,SAAX8B,0CAAW9B;;;QACrC,MAAMH,YAAYT,GAAG,CAACb,UAAUwD,GAAG,CAAC,aAC/BD;YACHE,WAAW/F,UAAUgG,GAAG;YACxBC,WAAWvD,QAAQc,GAAG;;QAExB,MAAM,CAAC0C,QAAQC,MAAM,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACxCxC,UAAU1D,kBAAkBkC,QAAQwB,SAASM,KAAK,CAAC,IAAO,CAAA;oBAAEC,OAAO,EAAE;gBAAa,CAAA,KAAM;gBAAEA,OAAO,EAAE;YAAC;YACpGjE,kBAAkBkC,QAAQ0B,SAASI,KAAK,CAAC,IAAO,CAAA;oBAAEC,OAAO,EAAE;gBAAa,CAAA;SACzE;QACD,MAAME,gBACJjC,QACA0B,SACA;eAAIoC,MAAM/B,KAAK;eAAMP,UAAU;gBAAC,CAAC,CAAC,EAAEA,QAAQ2B,IAAI,EAAE;aAAC,GAAG,EAAE;eAAMU,OAAO9B,KAAK;SAAC,EAC3E;QAEF,OAAO1C,SAASC,IAAI,CAAC;YAAEqC,IAAI;YAAMD;QAAQ,GAAG;YAAElC,QAAQ;QAAI;IAC5D,EAAE,OAAOD,OAAO;QACd,IAAI7B,iBAAiB6B,QAAQ;YAC3B,OAAOF,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAkB,GAAG;gBAAEC,QAAQ;YAAI;QACnE;QACAyE,QAAQ1E,KAAK,CAACA;QACd,OAAOF,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAmC,GAAG;YAAEC,QAAQ;QAAI;IACpF;AACF,EAAC;AAED;;;;;;;;;;CAUC,GACD,eAAeyC,gBACbjC,MAAc,EACd0B,OAAiC,EACjCK,KAAe,EACfmC,MAAc;IAEd,IAAI;QACF,MAAM9G,oBAAoB;YACxB+G,SAAS;gBAACnE;aAAO;YACjB+B,OAAO;gBAAE,CAAC/B,OAAO,EAAE;uBAAI,IAAIoE,IAAI;wBAAC,CAAC,CAAC,EAAE1C,QAAQyB,IAAI,EAAE;2BAAKpB;qBAAM;iBAAE;YAAC;YAChEmC;QACF;IACF,EAAE,OAAO3E,OAAO;QACd0E,QAAQ1E,KAAK,CAAC,oCAAoCS,QAAQT;IAC5D;IACA,IAAI;YAEY8C;QADd,MAAMA,UAAU,MAAMtE,gBAAgBiC,QAAQ0B,QAAQU,SAAS;QAC/D,MAAMhB,QAAQiB,4BAAAA,6BAAAA,QAASgC,GAAG,CAACC,MAAM,CAACA,MAAM,qBAA1BjC,2BAA4BS,EAAE;QAC5C,IAAIT,WAAWjB,OAAO;YACpB,MAAMxD,uBAAuB;gBAC3B+C,WAAWrD,cAAcgD,GAAG,GAAGK,SAAS;gBACxCS;gBACAgB,WAAWC,QAAQS,EAAE;YACvB;QACF;IACF,EAAE,OAAOvD,OAAO;QACd0E,QAAQ1E,KAAK,CAAC,0CAA0CS,QAAQT;IAClE;AACF"}