@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.
- package/README.md +9 -6
- package/package.json +10 -10
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
- package/src/lib/artifact/dataset-schema.d.ts +136 -0
- package/src/lib/artifact/dataset-schema.js +232 -0
- package/src/lib/artifact/dataset-schema.js.map +1 -0
- package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-record-dialog.component.js +1 -1
- package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
- package/src/lib/components/dataset-record-filter.d.ts +1 -1
- package/src/lib/components/dataset-record-filter.js +2 -2
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +55 -23
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +91 -403
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +12 -0
- package/src/lib/constants/bundle-common.js +13 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +16 -1
- package/src/lib/declarations.console-server.js +34 -2
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.server.d.ts +13 -0
- package/src/lib/declarations.server.js +48 -2
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
- package/src/lib/model/dataset-models.d.ts +392 -0
- package/src/lib/model/dataset-models.js +806 -0
- package/src/lib/model/dataset-models.js.map +1 -0
- package/src/lib/model/dataset-record-view.d.ts +1 -1
- package/src/lib/model/dataset-record-view.js +1 -2
- package/src/lib/model/dataset-record-view.js.map +1 -1
- package/src/lib/model/dataset-record.js +2 -2
- package/src/lib/model/dataset-record.js.map +1 -1
- package/src/lib/model/datasets.d.ts +162 -0
- package/src/lib/model/datasets.js +227 -0
- package/src/lib/model/datasets.js.map +1 -0
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +40 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
- package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
- package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
- package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
- package/src/lib/record-pages/record-page-read.server.js +175 -0
- package/src/lib/record-pages/record-page-read.server.js.map +1 -0
- package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
- package/src/lib/record-pages/record-page-resolver.server.js +131 -0
- package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
- package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
- package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
- package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
- package/src/lib/record-pages/record-page-source.d.ts +42 -0
- package/src/lib/record-pages/record-page-source.js +176 -0
- package/src/lib/record-pages/record-page-source.js.map +1 -0
- package/src/lib/record-pages/record-pages-route.d.ts +42 -0
- package/src/lib/record-pages/record-pages-route.js +348 -0
- package/src/lib/record-pages/record-pages-route.js.map +1 -0
- package/src/lib/record-pages/record-pages.d.ts +218 -0
- package/src/lib/record-pages/record-pages.js +351 -0
- package/src/lib/record-pages/record-pages.js.map +1 -0
- package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
- package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
- package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
- package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
- package/src/lib/record-pages/record-template-section.component.js +491 -0
- package/src/lib/record-pages/record-template-section.component.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
- package/src/lib/repeat/dataset-repeat-rows.js +30 -12
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
- package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat/repeat-record-pages.js +64 -0
- package/src/lib/repeat/repeat-record-pages.js.map +1 -0
- package/src/lib/server/announce-dataset-records.d.ts +1 -1
- package/src/lib/server/announce-dataset-records.js +1 -1
- package/src/lib/server/announce-dataset-records.js.map +1 -1
- package/src/lib/server/api-v1/datasets.js +5 -3
- package/src/lib/server/api-v1/datasets.js.map +1 -1
- package/src/lib/server/dataset-figures.js +2 -2
- package/src/lib/server/dataset-figures.js.map +1 -1
- package/src/lib/server/dataset-live-pages.d.ts +147 -0
- package/src/lib/server/dataset-live-pages.js +436 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dataset-steps.server.d.ts +45 -0
- package/src/lib/server/dataset-steps.server.js +268 -0
- package/src/lib/server/dataset-steps.server.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +2 -0
- package/src/lib/server/datasets-route.js +102 -6
- package/src/lib/server/datasets-route.js.map +1 -1
- package/src/lib/server/resolve-dataset.d.ts +40 -0
- package/src/lib/server/resolve-dataset.js +64 -0
- package/src/lib/server/resolve-dataset.js.map +1 -0
- package/src/lib/server.d.ts +3 -2
- package/src/lib/server.js +8 -9
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-bundle/datasets-package.d.ts +42 -0
- package/src/lib/site-bundle/datasets-package.js +95 -0
- package/src/lib/site-bundle/datasets-package.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
- package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
- package/src/lib/tenant-api.js +30 -0
- package/src/lib/tenant-api.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
- package/src/lib/transfer/dataset-transfer-key.js +31 -0
- package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
- package/src/lib/transfer/dataset-transfer-model.js +536 -0
- package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
- package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
- package/src/lib/transfer/dataset-transfer-resource.js +41 -0
- package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
- package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
- package/src/lib/transfer/dataset-transfer.server.js +930 -0
- package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +0 -36
- package/src/lib/model/dataset-csv.js +0 -85
- package/src/lib/model/dataset-csv.js.map +0 -1
- package/src/lib/model/dataset-io.d.ts +0 -44
- package/src/lib/model/dataset-io.js +0 -85
- package/src/lib/model/dataset-io.js.map +0 -1
- package/src/lib/model/index.js.map +0 -1
- package/src/lib/server/datasets-export-route.d.ts +0 -57
- package/src/lib/server/datasets-export-route.js +0 -229
- 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"}
|