@aglyn/plugins-data 1.0.0-beta.218 → 1.0.0-beta.220
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 +1 -1
- package/package.json +11 -9
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +6 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +47 -0
- package/src/lib/declarations.console-server.js +74 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +43 -0
- package/src/lib/declarations.server.js +73 -0
- package/src/lib/declarations.server.js.map +1 -0
- package/src/lib/form-target/dataset-form-record-target.server.d.ts +40 -0
- package/src/lib/form-target/dataset-form-record-target.server.js +160 -0
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -0
- package/src/lib/form-target/form-dataset-binding-token.d.ts +35 -0
- package/src/lib/form-target/form-dataset-binding-token.js +107 -0
- package/src/lib/form-target/form-dataset-binding-token.js.map +1 -0
- package/src/lib/form-target/form-dataset-binding.d.ts +50 -0
- package/src/lib/form-target/form-dataset-binding.js +74 -0
- package/src/lib/form-target/form-dataset-binding.js.map +1 -0
- package/src/lib/form-target/stamp-form-dataset-bindings.d.ts +39 -0
- package/src/lib/form-target/stamp-form-dataset-bindings.js +65 -0
- package/src/lib/form-target/stamp-form-dataset-bindings.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +36 -0
- package/src/lib/model/dataset-csv.js +85 -0
- package/src/lib/model/dataset-csv.js.map +1 -0
- package/src/lib/model/dataset-io.d.ts +8 -7
- package/src/lib/model/dataset-io.js +9 -8
- package/src/lib/model/dataset-io.js.map +1 -1
- package/src/lib/model/dataset-record-list.d.ts +40 -0
- package/src/lib/model/dataset-record-list.js +61 -0
- package/src/lib/model/dataset-record-list.js.map +1 -0
- package/src/lib/model/dataset-record.d.ts +33 -0
- package/src/lib/model/dataset-record.js +68 -0
- package/src/lib/model/dataset-record.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -2
- package/src/lib/repeat/dataset-repeat-rows.js +2 -2
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +43 -0
- package/src/lib/repeat/dataset-repeat-rows.server.js +170 -0
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-source.js +3 -1
- package/src/lib/repeat/dataset-repeat-source.js.map +1 -1
- package/src/lib/server/announce-dataset-records.d.ts +37 -0
- package/src/lib/server/announce-dataset-records.js +54 -0
- package/src/lib/server/announce-dataset-records.js.map +1 -0
- package/src/lib/server/api-v1/datasets.d.ts +18 -0
- package/src/lib/server/api-v1/datasets.js +646 -0
- package/src/lib/server/api-v1/datasets.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +26 -0
- package/src/lib/server/api-v1/openapi.js +260 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +33 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/custom-field-types.d.ts +39 -0
- package/src/lib/server/custom-field-types.js +41 -0
- package/src/lib/server/custom-field-types.js.map +1 -0
- package/src/lib/server/dataset-figures.d.ts +61 -0
- package/src/lib/server/dataset-figures.js +413 -0
- package/src/lib/server/dataset-figures.js.map +1 -0
- package/src/lib/server/dataset-record-index.d.ts +18 -0
- package/src/lib/server/dataset-record-index.js +62 -0
- package/src/lib/server/dataset-record-index.js.map +1 -0
- package/src/lib/server/dataset-storage-meter.d.ts +29 -0
- package/src/lib/server/dataset-storage-meter.js +82 -0
- package/src/lib/server/dataset-storage-meter.js.map +1 -0
- package/src/lib/server/datasets-export-route.d.ts +57 -0
- package/src/lib/server/datasets-export-route.js +229 -0
- package/src/lib/server/datasets-export-route.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +38 -0
- package/src/lib/server/datasets-route.js +614 -0
- package/src/lib/server/datasets-route.js.map +1 -0
- package/src/lib/server.d.ts +23 -0
- package/src/lib/server.js +54 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.d.ts +87 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +184 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -0
- package/src/lib/usage-axes.d.ts +24 -0
- package/src/lib/usage-axes.js +32 -0
- package/src/lib/usage-axes.js.map +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
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 PluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
18
|
+
/**
|
|
19
|
+
* The figures an insight reads about the workspace's datasets (AGL-2915),
|
|
20
|
+
* registered by the plugin that keeps them on the `plugin-figures` seam
|
|
21
|
+
* (AGL-3080), so the asking plugin reads no dataset itself.
|
|
22
|
+
*
|
|
23
|
+
* Each reader answers with one compact table of aggregates, read-only and
|
|
24
|
+
* about nobody: counts, sums and rates, labeled by a dataset's own field —
|
|
25
|
+
* never a record. A breakdown refuses a field with more than
|
|
26
|
+
* `DATASET_FIGURE_MAX_GROUP_VALUES` different values — on a dataset of any
|
|
27
|
+
* size, a name, an address or an order number — and folds every group of
|
|
28
|
+
* fewer than `DATASET_FIGURE_MIN_GROUP` records into one row, so each label it
|
|
29
|
+
* shows is a value at least that many records share.
|
|
30
|
+
*
|
|
31
|
+
* A request reads only the datasets the member who asked may see — and,
|
|
32
|
+
* narrowed to a site, only those shared with it, which is the site Data
|
|
33
|
+
* page's rule (AGL-2891). With no member, only datasets shared with every
|
|
34
|
+
* site.
|
|
35
|
+
*/
|
|
36
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
37
|
+
/** How a reader reaches Firestore: resolved when it reads, never when it registers. */
|
|
38
|
+
export type DatasetFigureFirestore = () => Firestore;
|
|
39
|
+
/** Datasets a dataset summary reads. */
|
|
40
|
+
export declare const DATASET_FIGURE_DATASETS_READ_LIMIT = 50;
|
|
41
|
+
/** Records a dataset breakdown or field summary reads, in document order; the rest are named in a note. */
|
|
42
|
+
export declare const DATASET_FIGURE_RECORDS_READ_LIMIT = 2000;
|
|
43
|
+
/** The fewest records a breakdown shows a group of by its own label. */
|
|
44
|
+
export declare const DATASET_FIGURE_MIN_GROUP = 3;
|
|
45
|
+
/**
|
|
46
|
+
* The most different values a breakdown groups by: every state and territory,
|
|
47
|
+
* a status, a category. A field with more tells records apart rather than
|
|
48
|
+
* sorting them, so it is refused.
|
|
49
|
+
*/
|
|
50
|
+
export declare const DATASET_FIGURE_MAX_GROUP_VALUES = 60;
|
|
51
|
+
/** The aggregates a breakdown computes. */
|
|
52
|
+
export declare const DATASET_FIGURE_OPERATIONS: readonly ["count", "sum", "average", "min", "max"];
|
|
53
|
+
export type DatasetFigureOperation = (typeof DATASET_FIGURE_OPERATIONS)[number];
|
|
54
|
+
export declare function datasetFigureReaders(firestore: DatasetFigureFirestore): PluginFigureReader[];
|
|
55
|
+
/**
|
|
56
|
+
* Registers them from the console's server declarations: the console is the
|
|
57
|
+
* one surface that runs insight jobs. Registering again replaces this
|
|
58
|
+
* plugin's own readers.
|
|
59
|
+
*/
|
|
60
|
+
export declare function registerDatasetFigureReaders(firestore: DatasetFigureFirestore): void;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { effectiveDatasetModel } from "@aglyn/aglyn/app-utils/dataset-models";
|
|
18
|
+
import { hostRoleFor, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/app-utils/organizations";
|
|
19
|
+
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
20
|
+
import { PLUGIN_FIGURE_MAX_ROWS, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
|
|
21
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
22
|
+
/** Datasets a dataset summary reads. */ export const DATASET_FIGURE_DATASETS_READ_LIMIT = 50;
|
|
23
|
+
/** Records a dataset breakdown or field summary reads, in document order; the rest are named in a note. */ export const DATASET_FIGURE_RECORDS_READ_LIMIT = 2000;
|
|
24
|
+
/** The fewest records a breakdown shows a group of by its own label. */ export const DATASET_FIGURE_MIN_GROUP = 3;
|
|
25
|
+
/**
|
|
26
|
+
* The most different values a breakdown groups by: every state and territory,
|
|
27
|
+
* a status, a category. A field with more tells records apart rather than
|
|
28
|
+
* sorting them, so it is refused.
|
|
29
|
+
*/ export const DATASET_FIGURE_MAX_GROUP_VALUES = 60;
|
|
30
|
+
/** The aggregates a breakdown computes. */ export const DATASET_FIGURE_OPERATIONS = [
|
|
31
|
+
'count',
|
|
32
|
+
'sum',
|
|
33
|
+
'average',
|
|
34
|
+
'min',
|
|
35
|
+
'max'
|
|
36
|
+
];
|
|
37
|
+
const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
38
|
+
const num = (value)=>{
|
|
39
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
40
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
41
|
+
};
|
|
42
|
+
const round1 = (value)=>Math.round(value * 10) / 10;
|
|
43
|
+
const share = (part, whole)=>whole > 0 ? round1(part / whole * 100) : null;
|
|
44
|
+
function refused(status, error) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
status,
|
|
48
|
+
error
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The datasets a request may read: those the member who asked may see — and,
|
|
53
|
+
* narrowed to a site, only those shared with it, which is the site Data page's
|
|
54
|
+
* rule (AGL-2891). With no member, only datasets shared with every site.
|
|
55
|
+
*/ async function visibleDatasets(firestore, request) {
|
|
56
|
+
const orgRef = firestore.collection('orgs').doc(request.orgId);
|
|
57
|
+
const [snapshot, memberSnapshot] = await Promise.all([
|
|
58
|
+
orgRef.collection('datasets').limit(DATASET_FIGURE_DATASETS_READ_LIMIT).get(),
|
|
59
|
+
request.uid ? orgRef.collection('members').doc(request.uid).get() : Promise.resolve(null)
|
|
60
|
+
]);
|
|
61
|
+
const member = (memberSnapshot == null ? void 0 : memberSnapshot.exists) ? memberSnapshot.data() : null;
|
|
62
|
+
if (request.uid && !member) return [];
|
|
63
|
+
if (request.hostId && member && !isOrgWideMember(member) && hostRoleFor(member, request.hostId) === null) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return snapshot.docs.map((doc)=>{
|
|
67
|
+
var _doc_data;
|
|
68
|
+
return {
|
|
69
|
+
id: doc.id,
|
|
70
|
+
data: (_doc_data = doc.data()) != null ? _doc_data : {}
|
|
71
|
+
};
|
|
72
|
+
}).filter(({ data })=>{
|
|
73
|
+
const visibleTo = data['visibleTo'];
|
|
74
|
+
if (data['deletedAt']) return false;
|
|
75
|
+
if (request.hostId && !visibleToHost(visibleTo, request.hostId)) return false;
|
|
76
|
+
return member ? memberCanSee(member, visibleTo) : Boolean(visibleTo == null ? void 0 : visibleTo.includes('org'));
|
|
77
|
+
}).map(({ id, data })=>({
|
|
78
|
+
id,
|
|
79
|
+
name: str(data['displayName']) || id,
|
|
80
|
+
model: effectiveDatasetModel(data)
|
|
81
|
+
})).sort((a, b)=>a.name.localeCompare(b.name));
|
|
82
|
+
}
|
|
83
|
+
async function readRecords(firestore, orgId, datasetId) {
|
|
84
|
+
const recordsRef = firestore.collection('orgs').doc(orgId).collection('datasets').doc(datasetId).collection('records');
|
|
85
|
+
const [page, count] = await Promise.all([
|
|
86
|
+
recordsRef.limit(DATASET_FIGURE_RECORDS_READ_LIMIT).get(),
|
|
87
|
+
recordsRef.count().get()
|
|
88
|
+
]);
|
|
89
|
+
return {
|
|
90
|
+
rows: page.docs.map((doc)=>{
|
|
91
|
+
var _values, _doc_data;
|
|
92
|
+
return (_values = ((_doc_data = doc.data()) != null ? _doc_data : {})['values']) != null ? _values : {};
|
|
93
|
+
}),
|
|
94
|
+
total: num(count.data().count)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const NUMERIC_TYPES = new Set([
|
|
98
|
+
'float',
|
|
99
|
+
'int32',
|
|
100
|
+
'int64'
|
|
101
|
+
]);
|
|
102
|
+
function readNote(read, total) {
|
|
103
|
+
return total > read ? [
|
|
104
|
+
`Read the first ${read.toLocaleString('en-US')} of ${total.toLocaleString('en-US')} records, so every figure covers only those.`
|
|
105
|
+
] : [];
|
|
106
|
+
}
|
|
107
|
+
/** A field a dataset table may be read by: the model's own id, or its name as the dataset page shows it. */ function fieldOf(model, raw) {
|
|
108
|
+
const wanted = raw.trim().toLowerCase();
|
|
109
|
+
for (const id of model.order){
|
|
110
|
+
var _model_fields_id;
|
|
111
|
+
if (id.toLowerCase() === wanted || str((_model_fields_id = model.fields[id]) == null ? void 0 : _model_fields_id.name).toLowerCase() === wanted) return id;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** A value as a breakdown groups it: text as written, a number or a yes/no as text; empty is its own group. */ function groupLabel(value) {
|
|
116
|
+
if (value === null || value === undefined || value === '') return '(empty)';
|
|
117
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
118
|
+
if (typeof value === 'number' || typeof value === 'string') return String(value).trim() || '(empty)';
|
|
119
|
+
return '(not a single value)';
|
|
120
|
+
}
|
|
121
|
+
export function datasetFigureReaders(firestore) {
|
|
122
|
+
const base = {
|
|
123
|
+
scope: 'org',
|
|
124
|
+
plugin: 'data',
|
|
125
|
+
feature: 'dataStore',
|
|
126
|
+
windows: []
|
|
127
|
+
};
|
|
128
|
+
// Datasets belong to the workspace, so a table links its Data page.
|
|
129
|
+
const source = {
|
|
130
|
+
label: 'Data',
|
|
131
|
+
path: 'data'
|
|
132
|
+
};
|
|
133
|
+
return [
|
|
134
|
+
_extends({}, base, {
|
|
135
|
+
id: 'datasets.summary',
|
|
136
|
+
label: 'Datasets',
|
|
137
|
+
description: 'With no dataset named: each dataset’s records and fields. With one: each of its fields, how many records fill it, how many distinct values it holds and, for a number, its lowest, average and highest.',
|
|
138
|
+
params: [
|
|
139
|
+
{
|
|
140
|
+
name: 'dataset',
|
|
141
|
+
description: 'A dataset’s id or name, to summarize its fields.',
|
|
142
|
+
required: false
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
read: async (request)=>{
|
|
146
|
+
const db = firestore();
|
|
147
|
+
const datasets = await visibleDatasets(db, request);
|
|
148
|
+
const named = request.params['dataset'];
|
|
149
|
+
if (!named) {
|
|
150
|
+
const counted = await Promise.all(datasets.slice(0, PLUGIN_FIGURE_MAX_ROWS).map(async (dataset)=>({
|
|
151
|
+
dataset: dataset.name,
|
|
152
|
+
records: num((await db.collection('orgs').doc(request.orgId).collection('datasets').doc(dataset.id).collection('records').count().get()).data().count),
|
|
153
|
+
fields: dataset.model.order.length
|
|
154
|
+
})));
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
table: {
|
|
158
|
+
title: 'Datasets',
|
|
159
|
+
source,
|
|
160
|
+
period: null,
|
|
161
|
+
columns: [
|
|
162
|
+
{
|
|
163
|
+
key: 'dataset',
|
|
164
|
+
label: 'Dataset',
|
|
165
|
+
kind: 'text'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: 'records',
|
|
169
|
+
label: 'Records',
|
|
170
|
+
kind: 'count'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: 'fields',
|
|
174
|
+
label: 'Fields',
|
|
175
|
+
kind: 'count'
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
rows: counted,
|
|
179
|
+
omitted: Math.max(0, datasets.length - counted.length),
|
|
180
|
+
notes: []
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const dataset = datasets.find((entry)=>entry.id === named || entry.name.toLowerCase() === named.toLowerCase());
|
|
185
|
+
if (!dataset) return refused(404, 'That dataset is not one you can read here');
|
|
186
|
+
const { rows: values, total } = await readRecords(db, request.orgId, dataset.id);
|
|
187
|
+
const rows = dataset.model.order.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((fieldId)=>{
|
|
188
|
+
var _ref;
|
|
189
|
+
const field = dataset.model.fields[fieldId];
|
|
190
|
+
const filled = values.filter((entry)=>entry[fieldId] !== null && entry[fieldId] !== undefined && entry[fieldId] !== '');
|
|
191
|
+
const numbers = NUMERIC_TYPES.has(String(field == null ? void 0 : field.type)) ? filled.map((entry)=>entry[fieldId]).filter((value)=>typeof value === 'number' && Number.isFinite(value)) : [];
|
|
192
|
+
return {
|
|
193
|
+
field: str(field == null ? void 0 : field.name) || fieldId,
|
|
194
|
+
type: String((_ref = field == null ? void 0 : field.type) != null ? _ref : 'text'),
|
|
195
|
+
filled: share(filled.length, values.length),
|
|
196
|
+
distinct: new Set(filled.map((entry)=>groupLabel(entry[fieldId]))).size,
|
|
197
|
+
lowest: numbers.length ? Math.min(...numbers) : null,
|
|
198
|
+
average: numbers.length ? Math.round(numbers.reduce((sum, value)=>sum + value, 0) / numbers.length * 100) / 100 : null,
|
|
199
|
+
highest: numbers.length ? Math.max(...numbers) : null
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
table: {
|
|
205
|
+
title: `${dataset.name}: fields`,
|
|
206
|
+
source,
|
|
207
|
+
period: null,
|
|
208
|
+
columns: [
|
|
209
|
+
{
|
|
210
|
+
key: 'field',
|
|
211
|
+
label: 'Field',
|
|
212
|
+
kind: 'text'
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
key: 'type',
|
|
216
|
+
label: 'Type',
|
|
217
|
+
kind: 'text'
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
key: 'filled',
|
|
221
|
+
label: 'Records that fill it',
|
|
222
|
+
kind: 'percent'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: 'distinct',
|
|
226
|
+
label: 'Distinct values',
|
|
227
|
+
kind: 'count'
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: 'lowest',
|
|
231
|
+
label: 'Lowest',
|
|
232
|
+
kind: 'number'
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: 'average',
|
|
236
|
+
label: 'Average',
|
|
237
|
+
kind: 'number'
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
key: 'highest',
|
|
241
|
+
label: 'Highest',
|
|
242
|
+
kind: 'number'
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
rows,
|
|
246
|
+
omitted: Math.max(0, dataset.model.order.length - rows.length),
|
|
247
|
+
notes: readNote(values.length, total)
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}),
|
|
252
|
+
_extends({}, base, {
|
|
253
|
+
id: 'datasets.breakdown',
|
|
254
|
+
label: 'Dataset breakdown',
|
|
255
|
+
description: 'One dataset’s records grouped by the values of one field, with how many records each group holds and, for a number field, its sum, average, lowest or highest in each group.',
|
|
256
|
+
params: [
|
|
257
|
+
{
|
|
258
|
+
name: 'dataset',
|
|
259
|
+
description: 'The dataset’s id or name.',
|
|
260
|
+
required: true
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'group',
|
|
264
|
+
description: 'The field to group records by, by id or name.',
|
|
265
|
+
required: true
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'measure',
|
|
269
|
+
description: 'A number field to aggregate, by id or name; empty to count records.',
|
|
270
|
+
required: false
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'operation',
|
|
274
|
+
description: `One of ${DATASET_FIGURE_OPERATIONS.join(', ')}; count when empty.`,
|
|
275
|
+
required: false
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
read: async (request)=>{
|
|
279
|
+
var _request_params_dataset, _request_params_group, _request_params_operation;
|
|
280
|
+
var _dataset_model_fields_measure, _dataset_model_fields_group, _dataset_model_fields_measure1;
|
|
281
|
+
const db = firestore();
|
|
282
|
+
const datasets = await visibleDatasets(db, request);
|
|
283
|
+
const named = (_request_params_dataset = request.params['dataset']) != null ? _request_params_dataset : '';
|
|
284
|
+
const dataset = datasets.find((entry)=>entry.id === named || entry.name.toLowerCase() === named.toLowerCase());
|
|
285
|
+
if (!dataset) return refused(404, 'That dataset is not one you can read here');
|
|
286
|
+
const group = fieldOf(dataset.model, (_request_params_group = request.params['group']) != null ? _request_params_group : '');
|
|
287
|
+
if (!group) return refused(400, 'That dataset has no field by that name to group by');
|
|
288
|
+
const operation = (_request_params_operation = request.params['operation']) != null ? _request_params_operation : 'count';
|
|
289
|
+
if (!DATASET_FIGURE_OPERATIONS.includes(operation)) {
|
|
290
|
+
return refused(400, `The operation is one of ${DATASET_FIGURE_OPERATIONS.join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
const measure = request.params['measure'] ? fieldOf(dataset.model, request.params['measure']) : null;
|
|
293
|
+
if (operation !== 'count' && (!measure || !NUMERIC_TYPES.has(String((_dataset_model_fields_measure = dataset.model.fields[measure]) == null ? void 0 : _dataset_model_fields_measure.type)))) {
|
|
294
|
+
return refused(400, 'Only a number field can be summed, averaged or ranged');
|
|
295
|
+
}
|
|
296
|
+
const { rows: values, total } = await readRecords(db, request.orgId, dataset.id);
|
|
297
|
+
const groups = new Map();
|
|
298
|
+
const sizes = new Map();
|
|
299
|
+
for (const entry of values){
|
|
300
|
+
var _sizes_get;
|
|
301
|
+
const label = groupLabel(entry[group]);
|
|
302
|
+
sizes.set(label, ((_sizes_get = sizes.get(label)) != null ? _sizes_get : 0) + 1);
|
|
303
|
+
const value = measure ? entry[measure] : null;
|
|
304
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
305
|
+
var _groups_get;
|
|
306
|
+
groups.set(label, [
|
|
307
|
+
...(_groups_get = groups.get(label)) != null ? _groups_get : [],
|
|
308
|
+
value
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (sizes.size > DATASET_FIGURE_MAX_GROUP_VALUES) {
|
|
313
|
+
return refused(400, `That field has more than ${DATASET_FIGURE_MAX_GROUP_VALUES} different values, too many to group by. Group by a field many records share, such as a state, a status or a category`);
|
|
314
|
+
}
|
|
315
|
+
// A group of fewer than DATASET_FIGURE_MIN_GROUP records is folded into
|
|
316
|
+
// one row: a group that small is a record, and its label could be a
|
|
317
|
+
// person's name.
|
|
318
|
+
const small = [
|
|
319
|
+
...sizes.entries()
|
|
320
|
+
].filter(([, size])=>size < DATASET_FIGURE_MIN_GROUP);
|
|
321
|
+
const aggregate = (numbers)=>{
|
|
322
|
+
if (operation === 'count') return null;
|
|
323
|
+
if (!numbers.length) return null;
|
|
324
|
+
const sum = numbers.reduce((acc, value)=>acc + value, 0);
|
|
325
|
+
switch(operation){
|
|
326
|
+
case 'sum':
|
|
327
|
+
return Math.round(sum * 100) / 100;
|
|
328
|
+
case 'average':
|
|
329
|
+
return Math.round(sum / numbers.length * 100) / 100;
|
|
330
|
+
case 'min':
|
|
331
|
+
return Math.min(...numbers);
|
|
332
|
+
default:
|
|
333
|
+
return Math.max(...numbers);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const row = (group, records, numbers)=>operation === 'count' ? {
|
|
337
|
+
group,
|
|
338
|
+
records
|
|
339
|
+
} : {
|
|
340
|
+
group,
|
|
341
|
+
records,
|
|
342
|
+
value: aggregate(numbers)
|
|
343
|
+
};
|
|
344
|
+
const rows = [
|
|
345
|
+
...sizes.entries()
|
|
346
|
+
].filter(([, size])=>size >= DATASET_FIGURE_MIN_GROUP).map(([label, size])=>{
|
|
347
|
+
var _groups_get;
|
|
348
|
+
return row(label, size, (_groups_get = groups.get(label)) != null ? _groups_get : []);
|
|
349
|
+
});
|
|
350
|
+
if (small.length) {
|
|
351
|
+
rows.push(row(`Other (${small.length} groups under ${DATASET_FIGURE_MIN_GROUP} records)`, small.reduce((sum, [, size])=>sum + size, 0), small.flatMap(([label])=>{
|
|
352
|
+
var _groups_get;
|
|
353
|
+
return (_groups_get = groups.get(label)) != null ? _groups_get : [];
|
|
354
|
+
})));
|
|
355
|
+
}
|
|
356
|
+
rows.sort((a, b)=>{
|
|
357
|
+
var _b_value, _a_value;
|
|
358
|
+
return num((_b_value = b['value']) != null ? _b_value : b['records']) - num((_a_value = a['value']) != null ? _a_value : a['records']);
|
|
359
|
+
});
|
|
360
|
+
const groupName = str((_dataset_model_fields_group = dataset.model.fields[group]) == null ? void 0 : _dataset_model_fields_group.name) || group;
|
|
361
|
+
const measureName = measure ? str((_dataset_model_fields_measure1 = dataset.model.fields[measure]) == null ? void 0 : _dataset_model_fields_measure1.name) || measure : '';
|
|
362
|
+
return {
|
|
363
|
+
ok: true,
|
|
364
|
+
table: {
|
|
365
|
+
title: `${dataset.name} by ${groupName}`,
|
|
366
|
+
source,
|
|
367
|
+
period: null,
|
|
368
|
+
columns: [
|
|
369
|
+
{
|
|
370
|
+
key: 'group',
|
|
371
|
+
label: groupName,
|
|
372
|
+
kind: 'text'
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
key: 'records',
|
|
376
|
+
label: 'Records',
|
|
377
|
+
kind: 'count'
|
|
378
|
+
},
|
|
379
|
+
...operation === 'count' ? [] : [
|
|
380
|
+
{
|
|
381
|
+
key: 'value',
|
|
382
|
+
label: `${operation[0].toUpperCase()}${operation.slice(1)} of ${measureName}`,
|
|
383
|
+
kind: 'number'
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
],
|
|
387
|
+
rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),
|
|
388
|
+
omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),
|
|
389
|
+
notes: [
|
|
390
|
+
...readNote(values.length, total),
|
|
391
|
+
...small.length ? [
|
|
392
|
+
`Groups of fewer than ${DATASET_FIGURE_MIN_GROUP} records are counted together as Other.`
|
|
393
|
+
] : []
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Registers them from the console's server declarations: the console is the
|
|
403
|
+
* one surface that runs insight jobs. Registering again replaces this
|
|
404
|
+
* plugin's own readers.
|
|
405
|
+
*/ export function registerDatasetFigureReaders(firestore) {
|
|
406
|
+
for (const reader of datasetFigureReaders(firestore)){
|
|
407
|
+
registerPluginFigureReader(reader, {
|
|
408
|
+
pluginId: BUNDLE_ID
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
//# sourceMappingURL=dataset-figures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-figures.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 { effectiveDatasetModel, type DatasetModel } from '@aglyn/aglyn/app-utils/dataset-models'\nimport { hostRoleFor, isOrgWideMember, memberCanSee } from '@aglyn/aglyn/app-utils/organizations'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n registerPluginFigureReader,\n type PluginFigureRead,\n type PluginFigureReader,\n type PluginFigureRequest,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * The figures an insight reads about the workspace's datasets (AGL-2915),\n * registered by the plugin that keeps them on the `plugin-figures` seam\n * (AGL-3080), so the asking plugin reads no dataset itself.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a dataset's own field —\n * never a record. A breakdown refuses a field with more than\n * `DATASET_FIGURE_MAX_GROUP_VALUES` different values — on a dataset of any\n * size, a name, an address or an order number — and folds every group of\n * fewer than `DATASET_FIGURE_MIN_GROUP` records into one row, so each label it\n * shows is a value at least that many records share.\n *\n * A request reads only the datasets the member who asked may see — and,\n * narrowed to a site, only those shared with it, which is the site Data\n * page's rule (AGL-2891). With no member, only datasets shared with every\n * site.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type DatasetFigureFirestore = () => Firestore\n\n/** Datasets a dataset summary reads. */\nexport const DATASET_FIGURE_DATASETS_READ_LIMIT = 50\n\n/** Records a dataset breakdown or field summary reads, in document order; the rest are named in a note. */\nexport const DATASET_FIGURE_RECORDS_READ_LIMIT = 2_000\n\n/** The fewest records a breakdown shows a group of by its own label. */\nexport const DATASET_FIGURE_MIN_GROUP = 3\n\n/**\n * The most different values a breakdown groups by: every state and territory,\n * a status, a category. A field with more tells records apart rather than\n * sorting them, so it is refused.\n */\nexport const DATASET_FIGURE_MAX_GROUP_VALUES = 60\n\n/** The aggregates a breakdown computes. */\nexport const DATASET_FIGURE_OPERATIONS = ['count', 'sum', 'average', 'min', 'max'] as const\nexport type DatasetFigureOperation = (typeof DATASET_FIGURE_OPERATIONS)[number]\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\ninterface VisibleDataset {\n id: string\n name: string\n model: DatasetModel\n}\n\n/**\n * The datasets a request may read: those the member who asked may see — and,\n * narrowed to a site, only those shared with it, which is the site Data page's\n * rule (AGL-2891). With no member, only datasets shared with every site.\n */\nasync function visibleDatasets(firestore: Firestore, request: PluginFigureRequest): Promise<VisibleDataset[]> {\n const orgRef = firestore.collection('orgs').doc(request.orgId)\n const [snapshot, memberSnapshot] = await Promise.all([\n orgRef.collection('datasets').limit(DATASET_FIGURE_DATASETS_READ_LIMIT).get(),\n request.uid ? orgRef.collection('members').doc(request.uid).get() : Promise.resolve(null),\n ])\n const member = memberSnapshot?.exists ? (memberSnapshot.data() as Partial<AglynOrgMember>) : null\n if (request.uid && !member) return []\n if (request.hostId && member && !isOrgWideMember(member) && hostRoleFor(member, request.hostId) === null) {\n return []\n }\n return snapshot.docs\n .map((doc) => ({ id: doc.id, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => {\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (data['deletedAt']) return false\n if (request.hostId && !visibleToHost(visibleTo, request.hostId)) return false\n return member ? memberCanSee(member, visibleTo) : Boolean(visibleTo?.includes('org'))\n })\n .map(({ id, data }) => ({\n id,\n name: str(data['displayName']) || id,\n model: effectiveDatasetModel(data as { model?: DatasetModel; fields?: string[] }),\n }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\nasync function readRecords(\n firestore: Firestore,\n orgId: string,\n datasetId: string,\n): Promise<{ rows: Array<Record<string, unknown>>; total: number }> {\n const recordsRef = firestore.collection('orgs').doc(orgId).collection('datasets').doc(datasetId).collection('records')\n const [page, count] = await Promise.all([\n recordsRef.limit(DATASET_FIGURE_RECORDS_READ_LIMIT).get(),\n recordsRef.count().get(),\n ])\n return {\n rows: page.docs.map((doc) => ((doc.data() ?? {})['values'] ?? {}) as Record<string, unknown>),\n total: num(count.data().count),\n }\n}\n\nconst NUMERIC_TYPES = new Set(['float', 'int32', 'int64'])\n\nfunction readNote(read: number, total: number): string[] {\n return total > read\n ? [`Read the first ${read.toLocaleString('en-US')} of ${total.toLocaleString('en-US')} records, so every figure covers only those.`]\n : []\n}\n\n/** A field a dataset table may be read by: the model's own id, or its name as the dataset page shows it. */\nfunction fieldOf(model: DatasetModel, raw: string): string | null {\n const wanted = raw.trim().toLowerCase()\n for (const id of model.order) {\n if (id.toLowerCase() === wanted || str(model.fields[id]?.name).toLowerCase() === wanted) return id\n }\n return null\n}\n\n/** A value as a breakdown groups it: text as written, a number or a yes/no as text; empty is its own group. */\nfunction groupLabel(value: unknown): string {\n if (value === null || value === undefined || value === '') return '(empty)'\n if (typeof value === 'boolean') return value ? 'Yes' : 'No'\n if (typeof value === 'number' || typeof value === 'string') return String(value).trim() || '(empty)'\n return '(not a single value)'\n}\n\nexport function datasetFigureReaders(firestore: DatasetFigureFirestore): PluginFigureReader[] {\n const base = { scope: 'org' as const, plugin: 'data', feature: 'dataStore', windows: [] as number[] }\n // Datasets belong to the workspace, so a table links its Data page.\n const source = { label: 'Data', path: 'data' }\n return [\n {\n ...base,\n id: 'datasets.summary',\n label: 'Datasets',\n description:\n 'With no dataset named: each dataset’s records and fields. With one: each of its fields, how many records fill it, how many distinct values it holds and, for a number, its lowest, average and highest.',\n params: [{ name: 'dataset', description: 'A dataset’s id or name, to summarize its fields.', required: false }],\n read: async (request) => {\n const db = firestore()\n const datasets = await visibleDatasets(db, request)\n const named = request.params['dataset']\n if (!named) {\n const counted = await Promise.all(\n datasets.slice(0, PLUGIN_FIGURE_MAX_ROWS).map(async (dataset) => ({\n dataset: dataset.name,\n records: num(\n (\n await db\n .collection('orgs')\n .doc(request.orgId)\n .collection('datasets')\n .doc(dataset.id)\n .collection('records')\n .count()\n .get()\n ).data().count,\n ),\n fields: dataset.model.order.length,\n })),\n )\n return {\n ok: true,\n table: {\n title: 'Datasets',\n source,\n period: null,\n columns: [\n { key: 'dataset', label: 'Dataset', kind: 'text' },\n { key: 'records', label: 'Records', kind: 'count' },\n { key: 'fields', label: 'Fields', kind: 'count' },\n ],\n rows: counted,\n omitted: Math.max(0, datasets.length - counted.length),\n notes: [],\n },\n }\n }\n const dataset = datasets.find((entry) => entry.id === named || entry.name.toLowerCase() === named.toLowerCase())\n if (!dataset) return refused(404, 'That dataset is not one you can read here')\n const { rows: values, total } = await readRecords(db, request.orgId, dataset.id)\n const rows = dataset.model.order.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((fieldId) => {\n const field = dataset.model.fields[fieldId]\n const filled = values.filter((entry) => entry[fieldId] !== null && entry[fieldId] !== undefined && entry[fieldId] !== '')\n const numbers = NUMERIC_TYPES.has(String(field?.type))\n ? filled.map((entry) => entry[fieldId]).filter((value): value is number => typeof value === 'number' && Number.isFinite(value))\n : []\n return {\n field: str(field?.name) || fieldId,\n type: String(field?.type ?? 'text'),\n filled: share(filled.length, values.length),\n distinct: new Set(filled.map((entry) => groupLabel(entry[fieldId]))).size,\n lowest: numbers.length ? Math.min(...numbers) : null,\n average: numbers.length ? Math.round((numbers.reduce((sum, value) => sum + value, 0) / numbers.length) * 100) / 100 : null,\n highest: numbers.length ? Math.max(...numbers) : null,\n }\n })\n return {\n ok: true,\n table: {\n title: `${dataset.name}: fields`,\n source,\n period: null,\n columns: [\n { key: 'field', label: 'Field', kind: 'text' },\n { key: 'type', label: 'Type', kind: 'text' },\n { key: 'filled', label: 'Records that fill it', kind: 'percent' },\n { key: 'distinct', label: 'Distinct values', kind: 'count' },\n { key: 'lowest', label: 'Lowest', kind: 'number' },\n { key: 'average', label: 'Average', kind: 'number' },\n { key: 'highest', label: 'Highest', kind: 'number' },\n ],\n rows,\n omitted: Math.max(0, dataset.model.order.length - rows.length),\n notes: readNote(values.length, total),\n },\n }\n },\n },\n {\n ...base,\n id: 'datasets.breakdown',\n label: 'Dataset breakdown',\n description:\n 'One dataset’s records grouped by the values of one field, with how many records each group holds and, for a number field, its sum, average, lowest or highest in each group.',\n params: [\n { name: 'dataset', description: 'The dataset’s id or name.', required: true },\n { name: 'group', description: 'The field to group records by, by id or name.', required: true },\n { name: 'measure', description: 'A number field to aggregate, by id or name; empty to count records.', required: false },\n { name: 'operation', description: `One of ${DATASET_FIGURE_OPERATIONS.join(', ')}; count when empty.`, required: false },\n ],\n read: async (request) => {\n const db = firestore()\n const datasets = await visibleDatasets(db, request)\n const named = request.params['dataset'] ?? ''\n const dataset = datasets.find((entry) => entry.id === named || entry.name.toLowerCase() === named.toLowerCase())\n if (!dataset) return refused(404, 'That dataset is not one you can read here')\n const group = fieldOf(dataset.model, request.params['group'] ?? '')\n if (!group) return refused(400, 'That dataset has no field by that name to group by')\n const operation = (request.params['operation'] ?? 'count') as DatasetFigureOperation\n if (!(DATASET_FIGURE_OPERATIONS as readonly string[]).includes(operation)) {\n return refused(400, `The operation is one of ${DATASET_FIGURE_OPERATIONS.join(', ')}`)\n }\n const measure = request.params['measure'] ? fieldOf(dataset.model, request.params['measure']) : null\n if (operation !== 'count' && (!measure || !NUMERIC_TYPES.has(String(dataset.model.fields[measure]?.type)))) {\n return refused(400, 'Only a number field can be summed, averaged or ranged')\n }\n const { rows: values, total } = await readRecords(db, request.orgId, dataset.id)\n const groups = new Map<string, number[]>()\n const sizes = new Map<string, number>()\n for (const entry of values) {\n const label = groupLabel(entry[group])\n sizes.set(label, (sizes.get(label) ?? 0) + 1)\n const value = measure ? entry[measure] : null\n if (typeof value === 'number' && Number.isFinite(value)) {\n groups.set(label, [...(groups.get(label) ?? []), value])\n }\n }\n if (sizes.size > DATASET_FIGURE_MAX_GROUP_VALUES) {\n return refused(\n 400,\n `That field has more than ${DATASET_FIGURE_MAX_GROUP_VALUES} different values, too many to group by. Group by a field many records share, such as a state, a status or a category`,\n )\n }\n // A group of fewer than DATASET_FIGURE_MIN_GROUP records is folded into\n // one row: a group that small is a record, and its label could be a\n // person's name.\n const small = [...sizes.entries()].filter(([, size]) => size < DATASET_FIGURE_MIN_GROUP)\n const aggregate = (numbers: number[]): number | null => {\n if (operation === 'count') return null\n if (!numbers.length) return null\n const sum = numbers.reduce((acc, value) => acc + value, 0)\n switch (operation) {\n case 'sum':\n return Math.round(sum * 100) / 100\n case 'average':\n return Math.round((sum / numbers.length) * 100) / 100\n case 'min':\n return Math.min(...numbers)\n default:\n return Math.max(...numbers)\n }\n }\n const row = (group: string, records: number, numbers: number[]): PluginFigureRow =>\n operation === 'count' ? { group, records } : { group, records, value: aggregate(numbers) }\n const rows: PluginFigureRow[] = [...sizes.entries()]\n .filter(([, size]) => size >= DATASET_FIGURE_MIN_GROUP)\n .map(([label, size]) => row(label, size, groups.get(label) ?? []))\n if (small.length) {\n rows.push(\n row(\n `Other (${small.length} groups under ${DATASET_FIGURE_MIN_GROUP} records)`,\n small.reduce((sum, [, size]) => sum + size, 0),\n small.flatMap(([label]) => groups.get(label) ?? []),\n ),\n )\n }\n rows.sort((a, b) => (num(b['value'] ?? b['records']) - num(a['value'] ?? a['records'])))\n const groupName = str(dataset.model.fields[group]?.name) || group\n const measureName = measure ? str(dataset.model.fields[measure]?.name) || measure : ''\n return {\n ok: true,\n table: {\n title: `${dataset.name} by ${groupName}`,\n source,\n period: null,\n columns: [\n { key: 'group', label: groupName, kind: 'text' },\n { key: 'records', label: 'Records', kind: 'count' },\n ...(operation === 'count'\n ? []\n : [{ key: 'value', label: `${operation[0].toUpperCase()}${operation.slice(1)} of ${measureName}`, kind: 'number' as const }]),\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n ...readNote(values.length, total),\n ...(small.length ? [`Groups of fewer than ${DATASET_FIGURE_MIN_GROUP} records are counted together as Other.`] : []),\n ],\n },\n }\n },\n },\n ]\n}\n\n/**\n * Registers them from the console's server declarations: the console is the\n * one surface that runs insight jobs. Registering again replaces this\n * plugin's own readers.\n */\nexport function registerDatasetFigureReaders(firestore: DatasetFigureFirestore): void {\n for (const reader of datasetFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["effectiveDatasetModel","hostRoleFor","isOrgWideMember","memberCanSee","visibleToHost","PLUGIN_FIGURE_MAX_ROWS","registerPluginFigureReader","BUNDLE_ID","DATASET_FIGURE_DATASETS_READ_LIMIT","DATASET_FIGURE_RECORDS_READ_LIMIT","DATASET_FIGURE_MIN_GROUP","DATASET_FIGURE_MAX_GROUP_VALUES","DATASET_FIGURE_OPERATIONS","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","visibleDatasets","firestore","request","orgRef","collection","doc","orgId","snapshot","memberSnapshot","Promise","all","limit","get","uid","resolve","member","exists","data","hostId","docs","map","id","filter","visibleTo","Boolean","includes","name","model","sort","a","b","localeCompare","readRecords","datasetId","recordsRef","page","count","rows","total","NUMERIC_TYPES","Set","readNote","read","toLocaleString","fieldOf","raw","wanted","toLowerCase","order","fields","groupLabel","undefined","String","datasetFigureReaders","base","scope","plugin","feature","windows","source","label","path","description","params","required","db","datasets","named","counted","slice","dataset","records","length","table","title","period","columns","key","kind","omitted","max","notes","find","entry","values","fieldId","field","filled","numbers","has","type","distinct","size","lowest","min","average","reduce","sum","highest","join","group","operation","measure","groups","Map","sizes","set","small","entries","aggregate","acc","row","push","flatMap","groupName","measureName","toUpperCase","registerDatasetFigureReaders","reader","pluginId"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 { PluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
|
|
18
|
+
export declare const datasetRecordIndex: PluginRecordIndex;
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
18
|
+
import { scopedToHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
19
|
+
import { datasetIndexedRecord } from "../model/dataset-record.js";
|
|
20
|
+
/**
|
|
21
|
+
* The data plugin's `dataset` index (AGL-3080): the workspace's datasets, as
|
|
22
|
+
* another surface reads them — an AI job drafting an automation that names a
|
|
23
|
+
* dataset, a planner listing what a site already has — without knowing where
|
|
24
|
+
* they are stored or what a deleted one looks like.
|
|
25
|
+
*
|
|
26
|
+
* Datasets belong to the ORGANIZATION (`orgId` required; a scope with none
|
|
27
|
+
* answers nothing). Narrowed to a site (`hostId`), only the datasets shared
|
|
28
|
+
* with it are answered — the site Data page's rule. A deleted dataset is left
|
|
29
|
+
* out of every answer.
|
|
30
|
+
*
|
|
31
|
+
* Each record is `datasetIndexedRecord`'s (`model/dataset-record.ts`): the
|
|
32
|
+
* name, the fields and the scope tokens, never a record of the dataset.
|
|
33
|
+
*/ function datasets(orgId) {
|
|
34
|
+
return firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('datasets');
|
|
35
|
+
}
|
|
36
|
+
export const datasetRecordIndex = {
|
|
37
|
+
async list ({ orgId, hostId, limit }) {
|
|
38
|
+
if (!orgId || limit <= 0) return {
|
|
39
|
+
records: [],
|
|
40
|
+
truncated: false
|
|
41
|
+
};
|
|
42
|
+
const collection = datasets(orgId);
|
|
43
|
+
const query = hostId ? scopedToHost(collection, hostId) : collection;
|
|
44
|
+
// One more than asked, so `truncated` is a fact rather than a guess;
|
|
45
|
+
// deleted datasets are filtered after the read.
|
|
46
|
+
const snapshot = await query.limit(limit + 1).get();
|
|
47
|
+
const records = snapshot.docs.map((doc)=>datasetIndexedRecord(doc.id, doc.data())).filter((record)=>record !== null);
|
|
48
|
+
return {
|
|
49
|
+
records: records.slice(0, limit),
|
|
50
|
+
truncated: snapshot.size > limit
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
async get ({ orgId, hostId, id }) {
|
|
54
|
+
if (!orgId || !id) return null;
|
|
55
|
+
const snapshot = await datasets(orgId).doc(id).get();
|
|
56
|
+
const record = snapshot.exists ? datasetIndexedRecord(snapshot.id, snapshot.data()) : null;
|
|
57
|
+
if (!record || !hostId) return record;
|
|
58
|
+
return visibleToHost(record.facts['visibleTo'], hostId) ? record : null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=dataset-record-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-record-index.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 { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type {\n PluginIndexedRecord,\n PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { datasetIndexedRecord } from '../model/dataset-record'\n\n/**\n * The data plugin's `dataset` index (AGL-3080): the workspace's datasets, as\n * another surface reads them — an AI job drafting an automation that names a\n * dataset, a planner listing what a site already has — without knowing where\n * they are stored or what a deleted one looks like.\n *\n * Datasets belong to the ORGANIZATION (`orgId` required; a scope with none\n * answers nothing). Narrowed to a site (`hostId`), only the datasets shared\n * with it are answered — the site Data page's rule. A deleted dataset is left\n * out of every answer.\n *\n * Each record is `datasetIndexedRecord`'s (`model/dataset-record.ts`): the\n * name, the fields and the scope tokens, never a record of the dataset.\n */\n\nfunction datasets(orgId: string) {\n return firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('datasets')\n}\n\nexport const datasetRecordIndex: PluginRecordIndex = {\n async list({ orgId, hostId, limit }) {\n if (!orgId || limit <= 0) return { records: [], truncated: false }\n const collection = datasets(orgId)\n const query = hostId ? scopedToHost(collection, hostId) : collection\n // One more than asked, so `truncated` is a fact rather than a guess;\n // deleted datasets are filtered after the read.\n const snapshot = await query.limit(limit + 1).get()\n const records = snapshot.docs\n .map((doc) => datasetIndexedRecord(doc.id, doc.data()))\n .filter((record): record is PluginIndexedRecord => record !== null)\n return { records: records.slice(0, limit), truncated: snapshot.size > limit }\n },\n async get({ orgId, hostId, id }) {\n if (!orgId || !id) return null\n const snapshot = await datasets(orgId).doc(id).get()\n const record = snapshot.exists ? datasetIndexedRecord(snapshot.id, snapshot.data()) : null\n if (!record || !hostId) return record\n return visibleToHost(record.facts['visibleTo'] as string[], hostId) ? record : null\n },\n}\n"],"names":["visibleToHost","firebaseAdmin","scopedToHost","datasetIndexedRecord","datasets","orgId","app","firestore","collection","doc","datasetRecordIndex","list","hostId","limit","records","truncated","query","snapshot","get","docs","map","id","data","filter","record","slice","size","exists","facts"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,sCAAqC;AAKnE,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,YAAY,QAAQ,gDAA+C;AAC5E,SAASC,oBAAoB,QAAQ,6BAAyB;AAE9D;;;;;;;;;;;;;CAaC,GAED,SAASC,SAASC,KAAa;IAC7B,OAAOJ,cAAcK,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAAC,QAAQC,GAAG,CAACJ,OAAOG,UAAU,CAAC;AAClF;AAEA,OAAO,MAAME,qBAAwC;IACnD,MAAMC,MAAK,EAAEN,KAAK,EAAEO,MAAM,EAAEC,KAAK,EAAE;QACjC,IAAI,CAACR,SAASQ,SAAS,GAAG,OAAO;YAAEC,SAAS,EAAE;YAAEC,WAAW;QAAM;QACjE,MAAMP,aAAaJ,SAASC;QAC5B,MAAMW,QAAQJ,SAASV,aAAaM,YAAYI,UAAUJ;QAC1D,qEAAqE;QACrE,gDAAgD;QAChD,MAAMS,WAAW,MAAMD,MAAMH,KAAK,CAACA,QAAQ,GAAGK,GAAG;QACjD,MAAMJ,UAAUG,SAASE,IAAI,CAC1BC,GAAG,CAAC,CAACX,MAAQN,qBAAqBM,IAAIY,EAAE,EAAEZ,IAAIa,IAAI,KAClDC,MAAM,CAAC,CAACC,SAA0CA,WAAW;QAChE,OAAO;YAAEV,SAASA,QAAQW,KAAK,CAAC,GAAGZ;YAAQE,WAAWE,SAASS,IAAI,GAAGb;QAAM;IAC9E;IACA,MAAMK,KAAI,EAAEb,KAAK,EAAEO,MAAM,EAAES,EAAE,EAAE;QAC7B,IAAI,CAAChB,SAAS,CAACgB,IAAI,OAAO;QAC1B,MAAMJ,WAAW,MAAMb,SAASC,OAAOI,GAAG,CAACY,IAAIH,GAAG;QAClD,MAAMM,SAASP,SAASU,MAAM,GAAGxB,qBAAqBc,SAASI,EAAE,EAAEJ,SAASK,IAAI,MAAM;QACtF,IAAI,CAACE,UAAU,CAACZ,QAAQ,OAAOY;QAC/B,OAAOxB,cAAcwB,OAAOI,KAAK,CAAC,YAAY,EAAchB,UAAUY,SAAS;IACjF;AACF,EAAC"}
|