@aglyn/plugins-data 1.0.0-beta.221 → 1.0.0-beta.223
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,227 @@
|
|
|
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
|
+
* Datasets (AGL-102): tabular data at `orgs/{orgId}/datasets/{id}` with a
|
|
18
|
+
* `records` subcollection, edited in the console Data card and consumed by
|
|
19
|
+
* repeatable components (AGL-103) via `{{item.field}}` bindings.
|
|
20
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
21
|
+
import { coerceDocumentValues, effectiveDatasetModel, humanizeDatasetFieldId, validateDocument } from "./dataset-models.js";
|
|
22
|
+
import { fillRecordAddresses } from "../record-pages/record-pages.js";
|
|
23
|
+
/** Dataset field name: starts with a letter; letters/digits/underscores. */ export const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
24
|
+
/**
|
|
25
|
+
* Stable field id from a human name: "Roast preference" → "roast_preference".
|
|
26
|
+
* Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.
|
|
27
|
+
*/ export function slugifyDatasetFieldId(name) {
|
|
28
|
+
const slug = name.trim().toLowerCase().replace(/[\s-]+/g, '_').replace(/[^a-z0-9_]/g, '').replace(/^[0-9_]+/, '').replace(/_+$/, '');
|
|
29
|
+
return DATASET_FIELD_PATTERN.test(slug) ? slug : '';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`
|
|
33
|
+
* slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`
|
|
34
|
+
* holds the ids already in use (compared case-insensitively). Returns '' when
|
|
35
|
+
* the name yields nothing valid. The user can override the result.
|
|
36
|
+
*/ export function defaultDatasetFieldId(name, taken) {
|
|
37
|
+
const base = slugifyDatasetFieldId(name);
|
|
38
|
+
if (!base) return '';
|
|
39
|
+
const used = new Set([
|
|
40
|
+
...taken
|
|
41
|
+
].map((id)=>id.toLowerCase()));
|
|
42
|
+
let candidate = base;
|
|
43
|
+
let suffix = 2;
|
|
44
|
+
while(used.has(candidate.toLowerCase()))candidate = `${base}_${suffix++}`;
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate a user-entered reference id (AGL-578): non-empty, matches
|
|
49
|
+
* `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already
|
|
50
|
+
* used, compared case-insensitively). Returns an error message, or null when
|
|
51
|
+
* the id is usable.
|
|
52
|
+
*/ export function validateDatasetFieldId(id, taken) {
|
|
53
|
+
const trimmed = id.trim();
|
|
54
|
+
if (!trimmed) return 'A reference ID is required';
|
|
55
|
+
if (!DATASET_FIELD_PATTERN.test(trimmed)) return 'Start with a letter; use only letters, numbers, and underscores';
|
|
56
|
+
const used = new Set([
|
|
57
|
+
...taken
|
|
58
|
+
].map((existing)=>existing.toLowerCase()));
|
|
59
|
+
if (used.has(trimmed.toLowerCase())) return 'Another field already uses this reference ID';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parses a comma/newline separated list of HUMAN field names into
|
|
64
|
+
* {id, name} entries — "Roast preference" keeps its pretty name and gets
|
|
65
|
+
* the stable id `roast_preference` (AGL-558). Plain snake_case keys
|
|
66
|
+
* still work and pick up a humanized display name. Duplicate ids and
|
|
67
|
+
* unsalvageable entries are dropped rather than failing the set.
|
|
68
|
+
*/ export function parseDatasetFieldEntries(input) {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const raw of input.split(/[,\n]/)){
|
|
72
|
+
const trimmed = raw.trim();
|
|
73
|
+
if (!trimmed) continue;
|
|
74
|
+
const id = slugifyDatasetFieldId(trimmed);
|
|
75
|
+
if (!id || seen.has(id)) continue;
|
|
76
|
+
seen.add(id);
|
|
77
|
+
entries.push({
|
|
78
|
+
id,
|
|
79
|
+
name: DATASET_FIELD_PATTERN.test(trimmed) ? humanizeDatasetFieldId(trimmed) : trimmed
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parses a comma/newline separated field list into valid unique names;
|
|
86
|
+
* invalid entries are dropped rather than failing the whole set.
|
|
87
|
+
*/ export function parseDatasetFields(input) {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const fields = [];
|
|
90
|
+
for (const raw of input.split(/[,\n]/)){
|
|
91
|
+
const name = raw.trim();
|
|
92
|
+
if (!DATASET_FIELD_PATTERN.test(name)) continue;
|
|
93
|
+
const key = name.toLowerCase();
|
|
94
|
+
if (seen.has(key)) continue;
|
|
95
|
+
seen.add(key);
|
|
96
|
+
fields.push(name);
|
|
97
|
+
}
|
|
98
|
+
return fields;
|
|
99
|
+
}
|
|
100
|
+
/** Record values restricted to the dataset's declared fields. */ export function sanitizeRecordValues(fields, input) {
|
|
101
|
+
const values = {};
|
|
102
|
+
for (const field of fields){
|
|
103
|
+
const value = input == null ? void 0 : input[field];
|
|
104
|
+
if (value == null) continue;
|
|
105
|
+
values[field] = String(value);
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The submitted values a dataset write would store, picked by field id and
|
|
111
|
+
* left as they arrived (AGL-556). With a `fieldMap` (submitted key → stable
|
|
112
|
+
* model fieldId), mapped values land under the mapped fieldId — so renamed
|
|
113
|
+
* schema fields keep receiving data — and entries whose fieldId isn't in the
|
|
114
|
+
* model are dropped. Submitted keys without a mapping (and calls without a
|
|
115
|
+
* map) fall back to the legacy name-intersection against the model's field
|
|
116
|
+
* ids, matching `sanitizeRecordValues`.
|
|
117
|
+
*/ export function pickDatasetRecordInput(dataset, input, fieldMap) {
|
|
118
|
+
const model = effectiveDatasetModel(dataset);
|
|
119
|
+
const map = fieldMap != null ? fieldMap : {};
|
|
120
|
+
const values = {};
|
|
121
|
+
for (const fieldId of model.order){
|
|
122
|
+
if (!model.fields[fieldId]) continue;
|
|
123
|
+
// A submitted key with an explicit mapping never doubles as a
|
|
124
|
+
// name-match — the mapping decides where it lands.
|
|
125
|
+
if (fieldId in map) continue;
|
|
126
|
+
const value = input == null ? void 0 : input[fieldId];
|
|
127
|
+
if (value == null) continue;
|
|
128
|
+
values[fieldId] = value;
|
|
129
|
+
}
|
|
130
|
+
for (const [submittedKey, fieldId] of Object.entries(map)){
|
|
131
|
+
if (typeof fieldId !== 'string' || !model.fields[fieldId]) continue;
|
|
132
|
+
const value = input == null ? void 0 : input[submittedKey];
|
|
133
|
+
if (value == null) continue;
|
|
134
|
+
values[fieldId] = value;
|
|
135
|
+
}
|
|
136
|
+
return values;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A text field takes what an event payload carries as a number or a boolean
|
|
140
|
+
* as the text it reads as, which is what these writes always stored.
|
|
141
|
+
*/ function asTextInput(model, picked) {
|
|
142
|
+
const values = {};
|
|
143
|
+
for (const [fieldId, value] of Object.entries(picked)){
|
|
144
|
+
var _model_fields_fieldId;
|
|
145
|
+
const type = (_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.type;
|
|
146
|
+
values[fieldId] = (type === 'text' || type === 'reference') && (typeof value === 'number' || typeof value === 'boolean') ? String(value) : value;
|
|
147
|
+
}
|
|
148
|
+
return values;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A record write from a form submission or an automation step, coerced and
|
|
152
|
+
* validated against the dataset's model (AGL-2773, option B).
|
|
153
|
+
*
|
|
154
|
+
* These writes used to store `String(value)` for every field and check
|
|
155
|
+
* nothing, so a form could put `abc` in a number column and skip a required
|
|
156
|
+
* one. They now run the same `coerceDocumentValues` + `validateDocument` pair
|
|
157
|
+
* the console and `/v1` do: a value is stored in its field's type, and a
|
|
158
|
+
* record with any refused field is not written at all. The caller decides
|
|
159
|
+
* what a refusal costs — a form keeps the Inbox submission, a run records a
|
|
160
|
+
* failed step.
|
|
161
|
+
*
|
|
162
|
+
* `existing` makes it a merge (update-or-append's update leg): this write's
|
|
163
|
+
* values are laid over the stored ones, and only the fields THIS write
|
|
164
|
+
* supplied are held to the model. A stored row written as text before this
|
|
165
|
+
* change keeps its text values and is not refused for them — the reads have
|
|
166
|
+
* always rendered them, and a merge that touched `email` must not fail over a
|
|
167
|
+
* legacy `price` of `"12"` it never sent.
|
|
168
|
+
*
|
|
169
|
+
* Custom field types validate only once registered: call
|
|
170
|
+
* `ensureDeclaredCustomFieldTypes(model)` before this on a server path.
|
|
171
|
+
*/ export function prepareDatasetRecordWrite(dataset, input, options = {}) {
|
|
172
|
+
const model = effectiveDatasetModel(dataset);
|
|
173
|
+
const picked = pickDatasetRecordInput(dataset, input, options.fieldMap);
|
|
174
|
+
const matched = Object.keys(picked);
|
|
175
|
+
const coerced = coerceDocumentValues(model, asTextInput(model, picked));
|
|
176
|
+
if (!options.existing) {
|
|
177
|
+
// A page address fills in from its source on the record's first write
|
|
178
|
+
// and is normalized on every one (AGL-3475).
|
|
179
|
+
const values = fillRecordAddresses(model, coerced);
|
|
180
|
+
return {
|
|
181
|
+
matched,
|
|
182
|
+
values,
|
|
183
|
+
errors: validateDocument(model, values)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const values = fillRecordAddresses(model, _extends({}, options.existing, coerced));
|
|
187
|
+
const errors = {};
|
|
188
|
+
for (const [fieldId, error] of Object.entries(validateDocument(model, values))){
|
|
189
|
+
if (fieldId in coerced) errors[fieldId] = error;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
matched,
|
|
193
|
+
values,
|
|
194
|
+
errors
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* A refused write's errors as one line, in the model's field order:
|
|
199
|
+
* `Stars must be a whole number; Email is required`.
|
|
200
|
+
*/ export function describeDatasetRecordErrors(errors) {
|
|
201
|
+
return Object.values(errors).join('; ');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The name a dataset is shown under: `displayName`, which is what every create
|
|
205
|
+
* path writes (AGL-536), then the pre-migration `name`. Blank when the document
|
|
206
|
+
* carries neither.
|
|
207
|
+
*/ export function datasetDisplayName(dataset) {
|
|
208
|
+
for (const candidate of [
|
|
209
|
+
dataset == null ? void 0 : dataset.displayName,
|
|
210
|
+
dataset == null ? void 0 : dataset.name
|
|
211
|
+
]){
|
|
212
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
213
|
+
return candidate.trim();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
/** Records sorted by their editor order (then id for stability). */ export function sortDatasetRecords(records) {
|
|
219
|
+
return [
|
|
220
|
+
...records
|
|
221
|
+
].sort((a, b)=>{
|
|
222
|
+
var _a_order, _b_order, _a_$id, _b_$id;
|
|
223
|
+
return ((_a_order = a.order) != null ? _a_order : Number.MAX_SAFE_INTEGER) - ((_b_order = b.order) != null ? _b_order : Number.MAX_SAFE_INTEGER) || String((_a_$id = a.$id) != null ? _a_$id : '').localeCompare(String((_b_$id = b.$id) != null ? _b_$id : ''));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
//# sourceMappingURL=datasets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/datasets.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Datasets (AGL-102): tabular data at `orgs/{orgId}/datasets/{id}` with a\n * `records` subcollection, edited in the console Data card and consumed by\n * repeatable components (AGL-103) via `{{item.field}}` bindings.\n */\n\nimport type { ScopeToken } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport {\n coerceDocumentValues,\n type DatasetFieldEntry,\n type DatasetModel,\n effectiveDatasetModel,\n humanizeDatasetFieldId,\n validateDocument,\n} from './dataset-models'\nimport { fillRecordAddresses } from '../record-pages/record-pages'\n\n/** Dataset field name: starts with a letter; letters/digits/underscores. */\nexport const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/\n\nexport interface HostDataset {\n $id?: string\n displayName?: string\n /** Column names, in display order (v1; superseded by `model`). */\n fields?: string[]\n /** Typed model (AGL-177); when absent, derive from `fields`. */\n model?: import('./dataset-models').DatasetModel\n /** dod.ts Schema.Name shape (AGL-178). */\n names?: { singular?: string; plural?: string }\n /**\n * Which sites may see this dataset (AGL-1037). Absent = org-wide, until\n * the AGL-1040 backfill stamps it. See `app-utils/scope-tokens`.\n */\n visibleTo?: ScopeToken[]\n}\n\nexport interface HostDatasetRecord {\n $id?: string\n /**\n * Field → value, in storage form per the field's type (see\n * `dataset-models`). Rows written by a form or an automation step before\n * AGL-2773 hold every value as text; readers accept both.\n */\n values?: Record<string, unknown>\n /** Row position in the editor and in repeated output. */\n order?: number\n}\n\n\n/**\n * Stable field id from a human name: \"Roast preference\" → \"roast_preference\".\n * Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.\n */\nexport function slugifyDatasetFieldId(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/^[0-9_]+/, '')\n .replace(/_+$/, '')\n return DATASET_FIELD_PATTERN.test(slug) ? slug : ''\n}\n\n/**\n * Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`\n * slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`\n * holds the ids already in use (compared case-insensitively). Returns '' when\n * the name yields nothing valid. The user can override the result.\n */\nexport function defaultDatasetFieldId(\n name: string,\n taken: ReadonlySet<string>,\n): string {\n const base = slugifyDatasetFieldId(name)\n if (!base) return ''\n const used = new Set([...taken].map((id) => id.toLowerCase()))\n let candidate = base\n let suffix = 2\n while (used.has(candidate.toLowerCase())) candidate = `${base}_${suffix++}`\n return candidate\n}\n\n/**\n * Validate a user-entered reference id (AGL-578): non-empty, matches\n * `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already\n * used, compared case-insensitively). Returns an error message, or null when\n * the id is usable.\n */\nexport function validateDatasetFieldId(\n id: string,\n taken: ReadonlySet<string>,\n): string | null {\n const trimmed = id.trim()\n if (!trimmed) return 'A reference ID is required'\n if (!DATASET_FIELD_PATTERN.test(trimmed))\n return 'Start with a letter; use only letters, numbers, and underscores'\n const used = new Set([...taken].map((existing) => existing.toLowerCase()))\n if (used.has(trimmed.toLowerCase()))\n return 'Another field already uses this reference ID'\n return null\n}\n\n\n/**\n * Parses a comma/newline separated list of HUMAN field names into\n * {id, name} entries — \"Roast preference\" keeps its pretty name and gets\n * the stable id `roast_preference` (AGL-558). Plain snake_case keys\n * still work and pick up a humanized display name. Duplicate ids and\n * unsalvageable entries are dropped rather than failing the set.\n */\nexport function parseDatasetFieldEntries(input: string): DatasetFieldEntry[] {\n const seen = new Set<string>()\n const entries: DatasetFieldEntry[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const trimmed = raw.trim()\n if (!trimmed) continue\n const id = slugifyDatasetFieldId(trimmed)\n if (!id || seen.has(id)) continue\n seen.add(id)\n entries.push({\n id,\n name: DATASET_FIELD_PATTERN.test(trimmed)\n ? humanizeDatasetFieldId(trimmed)\n : trimmed,\n })\n }\n return entries\n}\n\n/**\n * Parses a comma/newline separated field list into valid unique names;\n * invalid entries are dropped rather than failing the whole set.\n */\nexport function parseDatasetFields(input: string): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const raw of input.split(/[,\\n]/)) {\n const name = raw.trim()\n if (!DATASET_FIELD_PATTERN.test(name)) continue\n const key = name.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n fields.push(name)\n }\n return fields\n}\n\n/** Record values restricted to the dataset's declared fields. */\nexport function sanitizeRecordValues(\n fields: string[],\n input: Record<string, unknown> | null | undefined,\n): Record<string, string> {\n const values: Record<string, string> = {}\n for (const field of fields) {\n const value = input?.[field]\n if (value == null) continue\n values[field] = String(value)\n }\n return values\n}\n\n/**\n * The submitted values a dataset write would store, picked by field id and\n * left as they arrived (AGL-556). With a `fieldMap` (submitted key → stable\n * model fieldId), mapped values land under the mapped fieldId — so renamed\n * schema fields keep receiving data — and entries whose fieldId isn't in the\n * model are dropped. Submitted keys without a mapping (and calls without a\n * map) fall back to the legacy name-intersection against the model's field\n * ids, matching `sanitizeRecordValues`.\n */\nexport function pickDatasetRecordInput(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n fieldMap?: Record<string, string> | null,\n): Record<string, unknown> {\n const model = effectiveDatasetModel(dataset)\n const map = fieldMap ?? {}\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n if (!model.fields[fieldId]) continue\n // A submitted key with an explicit mapping never doubles as a\n // name-match — the mapping decides where it lands.\n if (fieldId in map) continue\n const value = input?.[fieldId]\n if (value == null) continue\n values[fieldId] = value\n }\n for (const [submittedKey, fieldId] of Object.entries(map)) {\n if (typeof fieldId !== 'string' || !model.fields[fieldId]) continue\n const value = input?.[submittedKey]\n if (value == null) continue\n values[fieldId] = value\n }\n return values\n}\n\n/** A dataset record write, checked against the dataset's model. */\nexport interface DatasetRecordWrite {\n /**\n * The model fields this write supplied a value for, before coercion. Empty\n * means nothing submitted matched a field of the dataset.\n */\n matched: string[]\n /**\n * The values to store, in storage form (see `dataset-models`): numbers,\n * booleans, epoch millis and so on per the field's type. For a merge, the\n * existing values with this write's laid over them.\n */\n values: Record<string, unknown>\n /** fieldId → the reason it was refused. Empty when the write may land. */\n errors: Record<string, string>\n}\n\n/**\n * A text field takes what an event payload carries as a number or a boolean\n * as the text it reads as, which is what these writes always stored.\n */\nfunction asTextInput(\n model: DatasetModel,\n picked: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const [fieldId, value] of Object.entries(picked)) {\n const type = model.fields[fieldId]?.type\n values[fieldId] =\n (type === 'text' || type === 'reference') &&\n (typeof value === 'number' || typeof value === 'boolean')\n ? String(value)\n : value\n }\n return values\n}\n\n/**\n * A record write from a form submission or an automation step, coerced and\n * validated against the dataset's model (AGL-2773, option B).\n *\n * These writes used to store `String(value)` for every field and check\n * nothing, so a form could put `abc` in a number column and skip a required\n * one. They now run the same `coerceDocumentValues` + `validateDocument` pair\n * the console and `/v1` do: a value is stored in its field's type, and a\n * record with any refused field is not written at all. The caller decides\n * what a refusal costs — a form keeps the Inbox submission, a run records a\n * failed step.\n *\n * `existing` makes it a merge (update-or-append's update leg): this write's\n * values are laid over the stored ones, and only the fields THIS write\n * supplied are held to the model. A stored row written as text before this\n * change keeps its text values and is not refused for them — the reads have\n * always rendered them, and a merge that touched `email` must not fail over a\n * legacy `price` of `\"12\"` it never sent.\n *\n * Custom field types validate only once registered: call\n * `ensureDeclaredCustomFieldTypes(model)` before this on a server path.\n */\nexport function prepareDatasetRecordWrite(\n dataset: { model?: DatasetModel; fields?: string[] },\n input: Record<string, unknown> | null | undefined,\n options: {\n fieldMap?: Record<string, string> | null\n existing?: Record<string, unknown> | null\n } = {},\n): DatasetRecordWrite {\n const model = effectiveDatasetModel(dataset)\n const picked = pickDatasetRecordInput(dataset, input, options.fieldMap)\n const matched = Object.keys(picked)\n const coerced = coerceDocumentValues(model, asTextInput(model, picked))\n if (!options.existing) {\n // A page address fills in from its source on the record's first write\n // and is normalized on every one (AGL-3475).\n const values = fillRecordAddresses(model, coerced)\n return {\n matched,\n values,\n errors: validateDocument(model, values),\n }\n }\n const values = fillRecordAddresses(model, { ...options.existing, ...coerced })\n const errors: Record<string, string> = {}\n for (const [fieldId, error] of Object.entries(\n validateDocument(model, values),\n )) {\n if (fieldId in coerced) errors[fieldId] = error\n }\n return { matched, values, errors }\n}\n\n/**\n * A refused write's errors as one line, in the model's field order:\n * `Stars must be a whole number; Email is required`.\n */\nexport function describeDatasetRecordErrors(\n errors: Record<string, string>,\n): string {\n return Object.values(errors).join('; ')\n}\n\n/**\n * The name a dataset is shown under: `displayName`, which is what every create\n * path writes (AGL-536), then the pre-migration `name`. Blank when the document\n * carries neither.\n */\nexport function datasetDisplayName(\n dataset: { displayName?: unknown; name?: unknown } | null | undefined,\n): string {\n for (const candidate of [dataset?.displayName, dataset?.name]) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return ''\n}\n\n/** Records sorted by their editor order (then id for stability). */\nexport function sortDatasetRecords<T extends HostDatasetRecord>(\n records: T[],\n): T[] {\n return [...records].sort(\n (a, b) =>\n (a.order ?? Number.MAX_SAFE_INTEGER) -\n (b.order ?? Number.MAX_SAFE_INTEGER) ||\n String(a.$id ?? '').localeCompare(String(b.$id ?? '')),\n )\n}\n"],"names":["coerceDocumentValues","effectiveDatasetModel","humanizeDatasetFieldId","validateDocument","fillRecordAddresses","DATASET_FIELD_PATTERN","slugifyDatasetFieldId","name","slug","trim","toLowerCase","replace","test","defaultDatasetFieldId","taken","base","used","Set","map","id","candidate","suffix","has","validateDatasetFieldId","trimmed","existing","parseDatasetFieldEntries","input","seen","entries","raw","split","add","push","parseDatasetFields","fields","key","sanitizeRecordValues","values","field","value","String","pickDatasetRecordInput","dataset","fieldMap","model","fieldId","order","submittedKey","Object","asTextInput","picked","type","prepareDatasetRecordWrite","options","matched","keys","coerced","errors","error","describeDatasetRecordErrors","join","datasetDisplayName","displayName","sortDatasetRecords","records","sort","a","b","Number","MAX_SAFE_INTEGER","$id","localeCompare"],"mappings":"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"}
|
package/src/lib/plugin.d.ts
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route
|
|
24
24
|
* that imports {@link HostDatasetsCard} directly.
|
|
25
25
|
*
|
|
26
|
+
* Record pages (AGL-3475) add a section to the besigner's Page Properties, a
|
|
27
|
+
* chip to the Pages list's rows, and the page-record source that draws a
|
|
28
|
+
* record template on the canvas for one of its records.
|
|
29
|
+
*
|
|
26
30
|
* It also registers datasets as a repeat source (AGL-3111), which is what puts
|
|
27
31
|
* "Repeat over dataset" on every element's Attributes panel and draws a
|
|
28
32
|
* repeat's copies from the real rows on the besigner canvas. Registered here,
|
package/src/lib/plugin.js
CHANGED
|
@@ -20,7 +20,14 @@ import { mdiDatabaseOutline } from "@aglyn/shared-data-mdi";
|
|
|
20
20
|
import { lazy } from "react";
|
|
21
21
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
22
22
|
import { registerDatasetRecordList } from "./model/dataset-record-list.js";
|
|
23
|
+
import { registerPageRecordSource } from "@aglyn/aglyn/app-utils/page-record-sources";
|
|
24
|
+
import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
|
|
25
|
+
import { DATASET_TRANSFER_RESOURCE } from "./transfer/dataset-transfer-key.js";
|
|
23
26
|
import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
|
|
27
|
+
import { RECORD_PAGE_ADDRESS_FIELD } from "./record-pages/record-pages.js";
|
|
28
|
+
import { RECORD_PAGE_SOURCE } from "./record-pages/record-page-source.js";
|
|
29
|
+
import RecordTemplateRowChip from "./record-pages/record-template-row-chip.component.js";
|
|
30
|
+
import RecordTemplateSection from "./record-pages/record-template-section.component.js";
|
|
24
31
|
/** Code-split: the Data console page only loads when opened. */ const DataConsolePage = lazy(()=>import("./components/data-console-page.js"));
|
|
25
32
|
/**
|
|
26
33
|
* Data feature plugin (AGL-395). Console-only — datasets are org-shared
|
|
@@ -31,6 +38,10 @@ import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
|
|
|
31
38
|
* entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route
|
|
32
39
|
* that imports {@link HostDatasetsCard} directly.
|
|
33
40
|
*
|
|
41
|
+
* Record pages (AGL-3475) add a section to the besigner's Page Properties, a
|
|
42
|
+
* chip to the Pages list's rows, and the page-record source that draws a
|
|
43
|
+
* record template on the canvas for one of its records.
|
|
44
|
+
*
|
|
34
45
|
* It also registers datasets as a repeat source (AGL-3111), which is what puts
|
|
35
46
|
* "Repeat over dataset" on every element's Attributes panel and draws a
|
|
36
47
|
* repeat's copies from the real rows on the besigner canvas. Registered here,
|
|
@@ -39,8 +50,25 @@ import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
|
|
|
39
50
|
* imported only to run one (AGL-3025).
|
|
40
51
|
*/ export function registerDataConsole() {
|
|
41
52
|
registerRepeatSource(DATASET_REPEAT_SOURCE);
|
|
53
|
+
// The "Page address" field type (AGL-3475), for the schema dialog's type
|
|
54
|
+
// list and the record editor's checks.
|
|
55
|
+
Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD);
|
|
56
|
+
// A record template draws on the canvas for one of its records (AGL-3475).
|
|
57
|
+
registerPageRecordSource(RECORD_PAGE_SOURCE);
|
|
42
58
|
// The workspace's datasets, for another plugin's picker (AGL-3080).
|
|
43
59
|
registerDatasetRecordList();
|
|
60
|
+
// A dataset's records in the import wizard and the export dialog
|
|
61
|
+
// (AGL-3530); the Data card names the dataset when it opens them.
|
|
62
|
+
registerPluginTransferResourceUi(DATASET_TRANSFER_RESOURCE, // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).
|
|
63
|
+
{
|
|
64
|
+
label: 'Dataset records',
|
|
65
|
+
icon: {
|
|
66
|
+
path: mdiDatabaseOutline.path
|
|
67
|
+
},
|
|
68
|
+
instancesFrom: 'dataset'
|
|
69
|
+
}, {
|
|
70
|
+
pluginId: BUNDLE_ID
|
|
71
|
+
});
|
|
44
72
|
Aglyn.registerConsoleExtension({
|
|
45
73
|
// Org datasets card (AGL-419): org/data renders it through the
|
|
46
74
|
// 'orgData' widget slot.
|
|
@@ -49,6 +77,18 @@ import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
|
|
|
49
77
|
slot: 'orgData',
|
|
50
78
|
widgetId: 'data-org-datasets',
|
|
51
79
|
Component: HostDatasetsCard
|
|
80
|
+
},
|
|
81
|
+
// Record pages (AGL-3475): the template's settings in Page Properties,
|
|
82
|
+
// and its chip in the Pages list.
|
|
83
|
+
{
|
|
84
|
+
slot: 'besignerPageProperties',
|
|
85
|
+
widgetId: 'data-record-template',
|
|
86
|
+
Component: RecordTemplateSection
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
slot: 'hostScreenRow',
|
|
90
|
+
widgetId: 'data-record-template-chip',
|
|
91
|
+
Component: RecordTemplateRowChip
|
|
52
92
|
}
|
|
53
93
|
],
|
|
54
94
|
pluginId: BUNDLE_ID,
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.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 HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\n\n/** Code-split: the Data console page only loads when opened. */\nconst DataConsolePage = lazy(() => import('./components/data-console-page'))\n\n/**\n * Data feature plugin (AGL-395). Console-only — datasets are org-shared\n * document collections consumed by repeatable components at render time\n * through binding resolution, not a canvas element of their own, so there\n * is no UI bundle. The console half declares the host Data nav + page\n * through the ConsoleExtension registry, gated by the `dataStore`\n * entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route\n * that imports {@link HostDatasetsCard} directly.\n *\n * It also registers datasets as a repeat source (AGL-3111), which is what puts\n * \"Repeat over dataset\" on every element's Attributes panel and draws a\n * repeat's copies from the real rows on the besigner canvas. Registered here,\n * inside the function the loader calls by name, and never by a module's own\n * load: this package declares no side effects, so a bundler deletes a module\n * imported only to run one (AGL-3025).\n */\nexport function registerDataConsole(): void {\n registerRepeatSource(DATASET_REPEAT_SOURCE)\n // The workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\n Aglyn.registerConsoleExtension({\n // Org datasets card (AGL-419): org/data renders it through the\n // 'orgData' widget slot.\n widgets: [\n {\n slot: 'orgData',\n widgetId: 'data-org-datasets',\n Component: HostDatasetsCard,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Data',\n featureFlag: 'dataStore',\n navItems: [\n {\n label: 'Data',\n href: '/data',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-data',\n icon: { path: mdiDatabaseOutline.path },\n header: {\n title: 'Data',\n icon: { path: mdiDatabaseOutline.path },\n docsTopic: 'datasets',\n },\n Component: DataConsolePage,\n },\n ],\n })\n}\n\nexport { default as HostDatasetsCard } from './components/host-datasets-card.component'\nexport type { HostDatasetsCardProps } from './components/host-datasets-card.component'\n"],"names":["HostDatasetsCard","Aglyn","registerRepeatSource","mdiDatabaseOutline","lazy","BUNDLE_ID","registerDatasetRecordList","DATASET_REPEAT_SOURCE","DataConsolePage","registerDataConsole","
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.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 HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { registerPageRecordSource } from '@aglyn/aglyn/app-utils/page-record-sources'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { DATASET_TRANSFER_RESOURCE } from './transfer/dataset-transfer-key'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { RECORD_PAGE_SOURCE } from './record-pages/record-page-source'\nimport RecordTemplateRowChip from './record-pages/record-template-row-chip.component'\nimport RecordTemplateSection from './record-pages/record-template-section.component'\n\n/** Code-split: the Data console page only loads when opened. */\nconst DataConsolePage = lazy(() => import('./components/data-console-page'))\n\n/**\n * Data feature plugin (AGL-395). Console-only — datasets are org-shared\n * document collections consumed by repeatable components at render time\n * through binding resolution, not a canvas element of their own, so there\n * is no UI bundle. The console half declares the host Data nav + page\n * through the ConsoleExtension registry, gated by the `dataStore`\n * entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route\n * that imports {@link HostDatasetsCard} directly.\n *\n * Record pages (AGL-3475) add a section to the besigner's Page Properties, a\n * chip to the Pages list's rows, and the page-record source that draws a\n * record template on the canvas for one of its records.\n *\n * It also registers datasets as a repeat source (AGL-3111), which is what puts\n * \"Repeat over dataset\" on every element's Attributes panel and draws a\n * repeat's copies from the real rows on the besigner canvas. Registered here,\n * inside the function the loader calls by name, and never by a module's own\n * load: this package declares no side effects, so a bundler deletes a module\n * imported only to run one (AGL-3025).\n */\nexport function registerDataConsole(): void {\n registerRepeatSource(DATASET_REPEAT_SOURCE)\n // The \"Page address\" field type (AGL-3475), for the schema dialog's type\n // list and the record editor's checks.\n Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // A record template draws on the canvas for one of its records (AGL-3475).\n registerPageRecordSource(RECORD_PAGE_SOURCE)\n // The workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\n // A dataset's records in the import wizard and the export dialog\n // (AGL-3530); the Data card names the dataset when it opens them.\n registerPluginTransferResourceUi(\n DATASET_TRANSFER_RESOURCE,\n // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).\n { label: 'Dataset records', icon: { path: mdiDatabaseOutline.path }, instancesFrom: 'dataset' },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Org datasets card (AGL-419): org/data renders it through the\n // 'orgData' widget slot.\n widgets: [\n {\n slot: 'orgData',\n widgetId: 'data-org-datasets',\n Component: HostDatasetsCard,\n },\n // Record pages (AGL-3475): the template's settings in Page Properties,\n // and its chip in the Pages list.\n {\n slot: 'besignerPageProperties',\n widgetId: 'data-record-template',\n Component: RecordTemplateSection,\n },\n {\n slot: 'hostScreenRow',\n widgetId: 'data-record-template-chip',\n Component: RecordTemplateRowChip,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Data',\n featureFlag: 'dataStore',\n navItems: [\n {\n label: 'Data',\n href: '/data',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-data',\n icon: { path: mdiDatabaseOutline.path },\n header: {\n title: 'Data',\n icon: { path: mdiDatabaseOutline.path },\n docsTopic: 'datasets',\n },\n Component: DataConsolePage,\n },\n ],\n })\n}\n\nexport { default as HostDatasetsCard } from './components/host-datasets-card.component'\nexport type { HostDatasetsCardProps } from './components/host-datasets-card.component'\n"],"names":["HostDatasetsCard","Aglyn","registerRepeatSource","mdiDatabaseOutline","lazy","BUNDLE_ID","registerDatasetRecordList","registerPageRecordSource","registerPluginTransferResourceUi","DATASET_TRANSFER_RESOURCE","DATASET_REPEAT_SOURCE","RECORD_PAGE_ADDRESS_FIELD","RECORD_PAGE_SOURCE","RecordTemplateRowChip","RecordTemplateSection","DataConsolePage","registerDataConsole","registerCustomFieldType","label","icon","path","instancesFrom","pluginId","registerConsoleExtension","widgets","slot","widgetId","Component","displayName","featureFlag","navItems","href","navTabId","header","title","docsTopic","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,OAAOA,sBAAsB,+CAA2C;AACxE,YAAYC,WAAW,eAAc;AACrC,SAASC,oBAAoB,QAAQ,wCAAuC;AAC5E,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,wBAAwB,QAAQ,6CAA4C;AACrF,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,yBAAyB,QAAQ,qCAAiC;AAC3E,SAASC,qBAAqB,QAAQ,oCAAgC;AACtE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,kBAAkB,QAAQ,uCAAmC;AACtE,OAAOC,2BAA2B,uDAAmD;AACrF,OAAOC,2BAA2B,sDAAkD;AAEpF,8DAA8D,GAC9D,MAAMC,kBAAkBX,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASY;IACdd,qBAAqBQ;IACrB,yEAAyE;IACzE,uCAAuC;IACvCT,MAAMgB,uBAAuB,CAACN;IAC9B,2EAA2E;IAC3EJ,yBAAyBK;IACzB,oEAAoE;IACpEN;IACA,iEAAiE;IACjE,kEAAkE;IAClEE,iCACEC,2BACA,wFAAwF;IACxF;QAAES,OAAO;QAAmBC,MAAM;YAAEC,MAAMjB,mBAAmBiB,IAAI;QAAC;QAAGC,eAAe;IAAU,GAC9F;QAAEC,UAAUjB;IAAU;IAExBJ,MAAMsB,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAW3B;YACb;YACA,uEAAuE;YACvE,kCAAkC;YAClC;gBACEyB,MAAM;gBACNC,UAAU;gBACVC,WAAWb;YACb;YACA;gBACEW,MAAM;gBACNC,UAAU;gBACVC,WAAWd;YACb;SACD;QACDS,UAAUjB;QACVuB,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEZ,OAAO;gBACPa,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVb,MAAM;oBAAEC,MAAMjB,mBAAmBiB,IAAI;gBAAC;gBACtCa,QAAQ;oBACNC,OAAO;oBACPf,MAAM;wBAAEC,MAAMjB,mBAAmBiB,IAAI;oBAAC;oBACtCe,WAAW;gBACb;gBACAR,WAAWZ;YACb;SACD;IACH;AACF;AAEA,SAASqB,WAAWpC,gBAAgB,QAAQ,+CAA2C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { PluginLivePathsReader } from '@aglyn/aglyn/plugin-manager/plugin-live-paths';
|
|
18
|
+
import { type DatasetRecordPageBinding } from './record-pages';
|
|
19
|
+
/**
|
|
20
|
+
* How many record pages one cache drop names per record template.
|
|
21
|
+
*
|
|
22
|
+
* A services or locations dataset fits inside it whole; a larger one names
|
|
23
|
+
* its first records by id and says it was cut short, and the rest refresh on
|
|
24
|
+
* their next visit through the host's data tag, which every drop busts first.
|
|
25
|
+
*/
|
|
26
|
+
export declare const RECORD_PAGE_DROP_LIMIT = 200;
|
|
27
|
+
/**
|
|
28
|
+
* A site's record templates read straight from Firestore, uncached — for the
|
|
29
|
+
* console's publish paths, which must see a binding the moment it is written,
|
|
30
|
+
* and which run outside the tenant's render cache anyway.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readRecordPageBindingsUncached(hostId: string): Promise<DatasetRecordPageBinding[]>;
|
|
33
|
+
/**
|
|
34
|
+
* One template's record pages, by the records' addresses: at most
|
|
35
|
+
* {@link RECORD_PAGE_DROP_LIMIT}, and `truncated` when there were more. A
|
|
36
|
+
* dataset the site may no longer see names none.
|
|
37
|
+
*/
|
|
38
|
+
export declare function recordPagePathsOf(hostId: string, binding: DatasetRecordPageBinding): Promise<{
|
|
39
|
+
paths: string[];
|
|
40
|
+
truncated: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* The record pages a dataset's change makes stale on one site: every page of
|
|
44
|
+
* every template there that renders the dataset.
|
|
45
|
+
*/
|
|
46
|
+
export declare function recordPagePathsForDataset(hostId: string, datasetId: string): Promise<{
|
|
47
|
+
paths: string[];
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* The record pages a publish makes stale (AGL-3475), answered on the
|
|
52
|
+
* platform's live-paths seam: the pages drawn by the screens being published,
|
|
53
|
+
* or by every record template on the site for a whole-site publish.
|
|
54
|
+
*/
|
|
55
|
+
export declare const recordPageLivePaths: PluginLivePathsReader;
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
17
|
+
import { FieldPath } from "firebase-admin/firestore";
|
|
18
|
+
import { readSiteDataset } from "./record-page-read.server.js";
|
|
19
|
+
import { DATASET_RECORD_PAGES_COLLECTION, RECORD_PAGES_MAX_PER_SITE, parseRecordPageBinding, recordAddressOf, recordPagePath } from "./record-pages.js";
|
|
20
|
+
/**
|
|
21
|
+
* How many record pages one cache drop names per record template.
|
|
22
|
+
*
|
|
23
|
+
* A services or locations dataset fits inside it whole; a larger one names
|
|
24
|
+
* its first records by id and says it was cut short, and the rest refresh on
|
|
25
|
+
* their next visit through the host's data tag, which every drop busts first.
|
|
26
|
+
*/ export const RECORD_PAGE_DROP_LIMIT = 200;
|
|
27
|
+
/**
|
|
28
|
+
* A site's record templates read straight from Firestore, uncached — for the
|
|
29
|
+
* console's publish paths, which must see a binding the moment it is written,
|
|
30
|
+
* and which run outside the tenant's render cache anyway.
|
|
31
|
+
*/ export async function readRecordPageBindingsUncached(hostId) {
|
|
32
|
+
const snapshot = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection(DATASET_RECORD_PAGES_COLLECTION).limit(RECORD_PAGES_MAX_PER_SITE).get();
|
|
33
|
+
return snapshot.docs.map((doc)=>parseRecordPageBinding(doc.id, doc.data())).filter((binding)=>binding != null);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One template's record pages, by the records' addresses: at most
|
|
37
|
+
* {@link RECORD_PAGE_DROP_LIMIT}, and `truncated` when there were more. A
|
|
38
|
+
* dataset the site may no longer see names none.
|
|
39
|
+
*/ export async function recordPagePathsOf(hostId, binding) {
|
|
40
|
+
const dataset = await readSiteDataset(hostId, binding.datasetId);
|
|
41
|
+
if (!dataset) return {
|
|
42
|
+
paths: [],
|
|
43
|
+
truncated: false
|
|
44
|
+
};
|
|
45
|
+
const rows = await dataset.ref.collection('records').orderBy(FieldPath.documentId()).limit(RECORD_PAGE_DROP_LIMIT + 1).select(`values.${binding.slugField}`).get();
|
|
46
|
+
const paths = new Set();
|
|
47
|
+
for (const row of rows.docs.slice(0, RECORD_PAGE_DROP_LIMIT)){
|
|
48
|
+
const address = recordAddressOf(row.get('values'), binding.slugField);
|
|
49
|
+
if (address) paths.add(recordPagePath(binding.base, address));
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
paths: [
|
|
53
|
+
...paths
|
|
54
|
+
],
|
|
55
|
+
truncated: rows.docs.length > RECORD_PAGE_DROP_LIMIT
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The record pages a dataset's change makes stale on one site: every page of
|
|
60
|
+
* every template there that renders the dataset.
|
|
61
|
+
*/ export async function recordPagePathsForDataset(hostId, datasetId) {
|
|
62
|
+
const bindings = (await readRecordPageBindingsUncached(hostId)).filter((binding)=>binding.datasetId === datasetId);
|
|
63
|
+
const answers = await Promise.all(bindings.map((binding)=>recordPagePathsOf(hostId, binding)));
|
|
64
|
+
return {
|
|
65
|
+
paths: [
|
|
66
|
+
...new Set(answers.flatMap((answer)=>answer.paths))
|
|
67
|
+
],
|
|
68
|
+
truncated: answers.some((answer)=>answer.truncated)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The record pages a publish makes stale (AGL-3475), answered on the
|
|
73
|
+
* platform's live-paths seam: the pages drawn by the screens being published,
|
|
74
|
+
* or by every record template on the site for a whole-site publish.
|
|
75
|
+
*/ export const recordPageLivePaths = async ({ hostId, screenIds })=>{
|
|
76
|
+
const bindings = (await readRecordPageBindingsUncached(hostId)).filter((binding)=>!screenIds || screenIds.includes(binding.screenId));
|
|
77
|
+
const answers = await Promise.all(bindings.map((binding)=>recordPagePathsOf(hostId, binding)));
|
|
78
|
+
return [
|
|
79
|
+
...new Set(answers.flatMap((answer)=>answer.paths))
|
|
80
|
+
];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=record-page-live-paths.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-page-live-paths.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginLivePathsReader } from '@aglyn/aglyn/plugin-manager/plugin-live-paths'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath } from 'firebase-admin/firestore'\nimport { readSiteDataset } from './record-page-read.server'\nimport {\n DATASET_RECORD_PAGES_COLLECTION,\n type DatasetRecordPageBinding,\n RECORD_PAGES_MAX_PER_SITE,\n parseRecordPageBinding,\n recordAddressOf,\n recordPagePath,\n} from './record-pages'\n\n/**\n * How many record pages one cache drop names per record template.\n *\n * A services or locations dataset fits inside it whole; a larger one names\n * its first records by id and says it was cut short, and the rest refresh on\n * their next visit through the host's data tag, which every drop busts first.\n */\nexport const RECORD_PAGE_DROP_LIMIT = 200\n\n/**\n * A site's record templates read straight from Firestore, uncached — for the\n * console's publish paths, which must see a binding the moment it is written,\n * and which run outside the tenant's render cache anyway.\n */\nexport async function readRecordPageBindingsUncached(\n hostId: string,\n): Promise<DatasetRecordPageBinding[]> {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection(DATASET_RECORD_PAGES_COLLECTION)\n .limit(RECORD_PAGES_MAX_PER_SITE)\n .get()\n return snapshot.docs\n .map((doc) => parseRecordPageBinding(doc.id, doc.data()))\n .filter((binding): binding is DatasetRecordPageBinding => binding != null)\n}\n\n/**\n * One template's record pages, by the records' addresses: at most\n * {@link RECORD_PAGE_DROP_LIMIT}, and `truncated` when there were more. A\n * dataset the site may no longer see names none.\n */\nexport async function recordPagePathsOf(\n hostId: string,\n binding: DatasetRecordPageBinding,\n): Promise<{ paths: string[]; truncated: boolean }> {\n const dataset = await readSiteDataset(hostId, binding.datasetId)\n if (!dataset) return { paths: [], truncated: false }\n const rows = await dataset.ref\n .collection('records')\n .orderBy(FieldPath.documentId())\n .limit(RECORD_PAGE_DROP_LIMIT + 1)\n .select(`values.${binding.slugField}`)\n .get()\n const paths = new Set<string>()\n for (const row of rows.docs.slice(0, RECORD_PAGE_DROP_LIMIT)) {\n const address = recordAddressOf(row.get('values'), binding.slugField)\n if (address) paths.add(recordPagePath(binding.base, address))\n }\n return { paths: [...paths], truncated: rows.docs.length > RECORD_PAGE_DROP_LIMIT }\n}\n\n/**\n * The record pages a dataset's change makes stale on one site: every page of\n * every template there that renders the dataset.\n */\nexport async function recordPagePathsForDataset(\n hostId: string,\n datasetId: string,\n): Promise<{ paths: string[]; truncated: boolean }> {\n const bindings = (await readRecordPageBindingsUncached(hostId)).filter(\n (binding) => binding.datasetId === datasetId,\n )\n const answers = await Promise.all(\n bindings.map((binding) => recordPagePathsOf(hostId, binding)),\n )\n return {\n paths: [...new Set(answers.flatMap((answer) => answer.paths))],\n truncated: answers.some((answer) => answer.truncated),\n }\n}\n\n/**\n * The record pages a publish makes stale (AGL-3475), answered on the\n * platform's live-paths seam: the pages drawn by the screens being published,\n * or by every record template on the site for a whole-site publish.\n */\nexport const recordPageLivePaths: PluginLivePathsReader = async ({ hostId, screenIds }) => {\n const bindings = (await readRecordPageBindingsUncached(hostId)).filter(\n (binding) => !screenIds || screenIds.includes(binding.screenId),\n )\n const answers = await Promise.all(\n bindings.map((binding) => recordPagePathsOf(hostId, binding)),\n )\n return [...new Set(answers.flatMap((answer) => answer.paths))]\n}\n"],"names":["firebaseAdmin","FieldPath","readSiteDataset","DATASET_RECORD_PAGES_COLLECTION","RECORD_PAGES_MAX_PER_SITE","parseRecordPageBinding","recordAddressOf","recordPagePath","RECORD_PAGE_DROP_LIMIT","readRecordPageBindingsUncached","hostId","snapshot","app","firestore","collection","doc","limit","get","docs","map","id","data","filter","binding","recordPagePathsOf","dataset","datasetId","paths","truncated","rows","ref","orderBy","documentId","select","slugField","Set","row","slice","address","add","base","length","recordPagePathsForDataset","bindings","answers","Promise","all","flatMap","answer","some","recordPageLivePaths","screenIds","includes","screenId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,eAAe,QAAQ,+BAA2B;AAC3D,SACEC,+BAA+B,EAE/BC,yBAAyB,EACzBC,sBAAsB,EACtBC,eAAe,EACfC,cAAc,QACT,oBAAgB;AAEvB;;;;;;CAMC,GACD,OAAO,MAAMC,yBAAyB,IAAG;AAEzC;;;;CAIC,GACD,OAAO,eAAeC,+BACpBC,MAAc;IAEd,MAAMC,WAAW,MAAMX,cACpBY,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACL,QACJI,UAAU,CAACX,iCACXa,KAAK,CAACZ,2BACNa,GAAG;IACN,OAAON,SAASO,IAAI,CACjBC,GAAG,CAAC,CAACJ,MAAQV,uBAAuBU,IAAIK,EAAE,EAAEL,IAAIM,IAAI,KACpDC,MAAM,CAAC,CAACC,UAAiDA,WAAW;AACzE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,kBACpBd,MAAc,EACda,OAAiC;IAEjC,MAAME,UAAU,MAAMvB,gBAAgBQ,QAAQa,QAAQG,SAAS;IAC/D,IAAI,CAACD,SAAS,OAAO;QAAEE,OAAO,EAAE;QAAEC,WAAW;IAAM;IACnD,MAAMC,OAAO,MAAMJ,QAAQK,GAAG,CAC3BhB,UAAU,CAAC,WACXiB,OAAO,CAAC9B,UAAU+B,UAAU,IAC5BhB,KAAK,CAACR,yBAAyB,GAC/ByB,MAAM,CAAC,CAAC,OAAO,EAAEV,QAAQW,SAAS,EAAE,EACpCjB,GAAG;IACN,MAAMU,QAAQ,IAAIQ;IAClB,KAAK,MAAMC,OAAOP,KAAKX,IAAI,CAACmB,KAAK,CAAC,GAAG7B,wBAAyB;QAC5D,MAAM8B,UAAUhC,gBAAgB8B,IAAInB,GAAG,CAAC,WAAWM,QAAQW,SAAS;QACpE,IAAII,SAASX,MAAMY,GAAG,CAAChC,eAAegB,QAAQiB,IAAI,EAAEF;IACtD;IACA,OAAO;QAAEX,OAAO;eAAIA;SAAM;QAAEC,WAAWC,KAAKX,IAAI,CAACuB,MAAM,GAAGjC;IAAuB;AACnF;AAEA;;;CAGC,GACD,OAAO,eAAekC,0BACpBhC,MAAc,EACdgB,SAAiB;IAEjB,MAAMiB,WAAW,AAAC,CAAA,MAAMlC,+BAA+BC,OAAM,EAAGY,MAAM,CACpE,CAACC,UAAYA,QAAQG,SAAS,KAAKA;IAErC,MAAMkB,UAAU,MAAMC,QAAQC,GAAG,CAC/BH,SAASxB,GAAG,CAAC,CAACI,UAAYC,kBAAkBd,QAAQa;IAEtD,OAAO;QACLI,OAAO;eAAI,IAAIQ,IAAIS,QAAQG,OAAO,CAAC,CAACC,SAAWA,OAAOrB,KAAK;SAAG;QAC9DC,WAAWgB,QAAQK,IAAI,CAAC,CAACD,SAAWA,OAAOpB,SAAS;IACtD;AACF;AAEA;;;;CAIC,GACD,OAAO,MAAMsB,sBAA6C,OAAO,EAAExC,MAAM,EAAEyC,SAAS,EAAE;IACpF,MAAMR,WAAW,AAAC,CAAA,MAAMlC,+BAA+BC,OAAM,EAAGY,MAAM,CACpE,CAACC,UAAY,CAAC4B,aAAaA,UAAUC,QAAQ,CAAC7B,QAAQ8B,QAAQ;IAEhE,MAAMT,UAAU,MAAMC,QAAQC,GAAG,CAC/BH,SAASxB,GAAG,CAAC,CAACI,UAAYC,kBAAkBd,QAAQa;IAEtD,OAAO;WAAI,IAAIY,IAAIS,QAAQG,OAAO,CAAC,CAACC,SAAWA,OAAOrB,KAAK;KAAG;AAChE,EAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 RepeatableDataset } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type DatasetModel } from '../model/dataset-models';
|
|
19
|
+
import { type DatasetRecordPageBinding } from './record-pages';
|
|
20
|
+
/**
|
|
21
|
+
* A site's record templates (AGL-3475), read once per render cache window and
|
|
22
|
+
* dropped with everything else the site caches under `tenant-data:{hostId}`,
|
|
23
|
+
* which every binding change busts.
|
|
24
|
+
*
|
|
25
|
+
* Asked on every request no published page claims, so it is one small cached
|
|
26
|
+
* read, bounded by {@link RECORD_PAGES_MAX_PER_SITE}. A site with none caches
|
|
27
|
+
* the empty list, which is the answer for almost every site.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readSiteRecordPageBindings(hostId: string): Promise<DatasetRecordPageBinding[]>;
|
|
30
|
+
/** A dataset this site may see, with its model, or `null`. */
|
|
31
|
+
export interface SiteDataset {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
model: DatasetModel;
|
|
35
|
+
ref: FirebaseFirestore.DocumentReference;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The dataset a binding names, when THIS site may see it — the rule the
|
|
39
|
+
* published repeat reader applies (`visibleTo`, AGL-1039), checked on the
|
|
40
|
+
* document because a keyed read bypasses the scoped query. A deleted dataset,
|
|
41
|
+
* or one no longer shared with the site, is `null`, and its record pages stop
|
|
42
|
+
* serving rather than serving what the site may no longer show.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readSiteDataset(hostId: string, datasetId: string): Promise<SiteDataset | null>;
|
|
45
|
+
/** One routed record, ready for the template: its row and its one hop. */
|
|
46
|
+
export interface RecordPageRecord {
|
|
47
|
+
dataset: {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
model: DatasetModel;
|
|
51
|
+
};
|
|
52
|
+
/** The record's values, with `$id` inside like a repeated row. */
|
|
53
|
+
record: Record<string, unknown>;
|
|
54
|
+
/** The datasets its reference fields point into, for one-hop tokens. */
|
|
55
|
+
datasetsByKey: Record<string, RepeatableDataset>;
|
|
56
|
+
/** The record's write time, for the sitemap's and the head's dates. */
|
|
57
|
+
updatedAt?: unknown;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The record a binding serves at `address`, or `null` — an unknown address,
|
|
61
|
+
* a dataset the site may not see, a slug field the model no longer has.
|
|
62
|
+
*
|
|
63
|
+
* Found through `filterValues.<slugField>`, the equality index every record
|
|
64
|
+
* write keeps (AGL-3321): a page address is stored normalized, which is
|
|
65
|
+
* exactly the key a plain text value's entry holds, so the lookup needs no
|
|
66
|
+
* index of its own. The candidates are then matched on the stored value
|
|
67
|
+
* itself, and when two records share an address the one first in the
|
|
68
|
+
* dataset's own order serves — the order a repeat lists them in.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readRecordPageRecord(hostId: string, binding: DatasetRecordPageBinding, address: string): Promise<RecordPageRecord | null>;
|