@aglyn/plugins-data 1.0.0-beta.234 → 1.0.0-beta.236
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/package.json +10 -10
- package/src/lib/components/dataset-record-filter.d.ts +10 -1
- package/src/lib/components/dataset-record-filter.js +12 -1
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +2 -11
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +19 -32
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/model/dataset-model-core.d.ts +299 -0
- package/src/lib/model/dataset-model-core.js +511 -0
- package/src/lib/model/dataset-model-core.js.map +1 -0
- package/src/lib/model/dataset-models.d.ts +2 -231
- package/src/lib/model/dataset-models.js +16 -392
- package/src/lib/model/dataset-models.js.map +1 -1
- package/src/lib/model/dataset-record-delete-plan.d.ts +60 -0
- package/src/lib/model/dataset-record-delete-plan.js +82 -0
- package/src/lib/model/dataset-record-delete-plan.js.map +1 -0
- package/src/lib/model/datasets.d.ts +2 -39
- package/src/lib/model/datasets.js +5 -77
- package/src/lib/model/datasets.js.map +1 -1
- package/src/lib/server/datasets-route.d.ts +3 -0
- package/src/lib/server/datasets-route.js +146 -3
- package/src/lib/server/datasets-route.js.map +1 -1
|
@@ -0,0 +1,511 @@
|
|
|
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
|
+
* The pure half of the dataset model (AGL-3668): the model and field types,
|
|
18
|
+
* the v1 shim, the display and input forms of a stored value, and the
|
|
19
|
+
* records table filter grammar (`filterKeys`/`filterValues` and the tokens a
|
|
20
|
+
* clause or a search word asks for). No imports, so the native apps generate
|
|
21
|
+
* from it (tools/scripts/mobile-pure-modules.json) and replay its answers.
|
|
22
|
+
* `dataset-models.ts` re-exports all of it beside the validation and
|
|
23
|
+
* coercion that reach the plugin registry.
|
|
24
|
+
*/ /**
|
|
25
|
+
* A human field entry from the quick creator: the stable id plus the
|
|
26
|
+
* display name shown in table headers and bindings pickers (AGL-558).
|
|
27
|
+
*/ /** Display fallback for raw ids: "roast_preference" → "Roast preference". */ export function humanizeDatasetFieldId(id) {
|
|
28
|
+
const words = id.replace(/_/g, ' ').trim();
|
|
29
|
+
return words ? words.charAt(0).toUpperCase() + words.slice(1) : id;
|
|
30
|
+
}
|
|
31
|
+
export const DATASET_FIELD_TYPES = [
|
|
32
|
+
'text',
|
|
33
|
+
'bool',
|
|
34
|
+
'int32',
|
|
35
|
+
'int64',
|
|
36
|
+
'float',
|
|
37
|
+
'timestamp',
|
|
38
|
+
'coordinates',
|
|
39
|
+
'map',
|
|
40
|
+
'sorted',
|
|
41
|
+
'bytes',
|
|
42
|
+
'nil',
|
|
43
|
+
'reference'
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* The types a person picks for a new field in the Schema dialog; the rest
|
|
47
|
+
* exist for compatibility and stay editable on a field that already has one.
|
|
48
|
+
*/ export const DATASET_AUTHORABLE_FIELD_TYPES = [
|
|
49
|
+
'text',
|
|
50
|
+
'bool',
|
|
51
|
+
'int32',
|
|
52
|
+
'float',
|
|
53
|
+
'timestamp',
|
|
54
|
+
'coordinates',
|
|
55
|
+
'sorted',
|
|
56
|
+
'reference'
|
|
57
|
+
];
|
|
58
|
+
/** Display names, mirroring dod.ts `lbl`. */ export const DATASET_FIELD_TYPE_LABELS = {
|
|
59
|
+
text: 'Text',
|
|
60
|
+
bool: 'Boolean',
|
|
61
|
+
int32: 'Integer',
|
|
62
|
+
int64: 'Big integer',
|
|
63
|
+
float: 'Number',
|
|
64
|
+
timestamp: 'Date & time',
|
|
65
|
+
coordinates: 'Coordinates',
|
|
66
|
+
map: 'Map',
|
|
67
|
+
sorted: 'List',
|
|
68
|
+
bytes: 'Bytes',
|
|
69
|
+
nil: 'Null',
|
|
70
|
+
reference: 'Reference'
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Migration shim (v1 → v2): datasets created before models carry flat
|
|
74
|
+
* `fields: string[]` — derive a model where every column is an optional
|
|
75
|
+
* text field whose id is the column name, so the editor and AGL-103
|
|
76
|
+
* bindings keep working on unmigrated docs during rollout.
|
|
77
|
+
*/ export function deriveModelFromFields(fields) {
|
|
78
|
+
const model = {
|
|
79
|
+
fields: {},
|
|
80
|
+
order: []
|
|
81
|
+
};
|
|
82
|
+
for (const name of fields){
|
|
83
|
+
// Raw snake_case ids read as titles in table headers and pickers;
|
|
84
|
+
// the id stays the stable key (AGL-558).
|
|
85
|
+
model.fields[name] = {
|
|
86
|
+
name: humanizeDatasetFieldId(name),
|
|
87
|
+
type: 'text'
|
|
88
|
+
};
|
|
89
|
+
model.order.push(name);
|
|
90
|
+
}
|
|
91
|
+
return model;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Model from quick-creator entries (AGL-558): stable slug ids with the
|
|
95
|
+
* human display names preserved — "Roast preference" → id
|
|
96
|
+
* `roast_preference`, name "Roast preference".
|
|
97
|
+
*/ export function modelFromFieldEntries(entries) {
|
|
98
|
+
const model = {
|
|
99
|
+
fields: {},
|
|
100
|
+
order: []
|
|
101
|
+
};
|
|
102
|
+
for (const entry of entries){
|
|
103
|
+
model.fields[entry.id] = {
|
|
104
|
+
name: entry.name,
|
|
105
|
+
type: 'text'
|
|
106
|
+
};
|
|
107
|
+
model.order.push(entry.id);
|
|
108
|
+
}
|
|
109
|
+
return model;
|
|
110
|
+
}
|
|
111
|
+
/** The dataset's model, deriving one from v1 `fields` when absent. */ export function effectiveDatasetModel(dataset) {
|
|
112
|
+
var _dataset_fields;
|
|
113
|
+
var _dataset_model, _dataset_model_order;
|
|
114
|
+
if (((_dataset_model = dataset.model) == null ? void 0 : _dataset_model.fields) && ((_dataset_model_order = dataset.model.order) == null ? void 0 : _dataset_model_order.length)) {
|
|
115
|
+
return dataset.model;
|
|
116
|
+
}
|
|
117
|
+
return deriveModelFromFields((_dataset_fields = dataset.fields) != null ? _dataset_fields : []);
|
|
118
|
+
}
|
|
119
|
+
const isFiniteNumber = (value)=>typeof value === 'number' && Number.isFinite(value);
|
|
120
|
+
/** Storage value → grid display string (AGL-179). */ export function formatDatasetValue(field, value) {
|
|
121
|
+
if (value == null || value === '') return '';
|
|
122
|
+
switch(field.type){
|
|
123
|
+
case 'bool':
|
|
124
|
+
return value === true ? '✓' : value === false ? '—' : String(value);
|
|
125
|
+
case 'timestamp':
|
|
126
|
+
return isFiniteNumber(value) ? new Date(value).toISOString().slice(0, 16).replace('T', ' ') : String(value);
|
|
127
|
+
case 'coordinates':
|
|
128
|
+
{
|
|
129
|
+
const coordinates = value;
|
|
130
|
+
return isFiniteNumber(coordinates == null ? void 0 : coordinates.latitude) && isFiniteNumber(coordinates == null ? void 0 : coordinates.longitude) ? `${coordinates.latitude}, ${coordinates.longitude}` : String(value);
|
|
131
|
+
}
|
|
132
|
+
case 'sorted':
|
|
133
|
+
return Array.isArray(value) ? value.join(', ') : String(value);
|
|
134
|
+
case 'map':
|
|
135
|
+
try {
|
|
136
|
+
return JSON.stringify(value);
|
|
137
|
+
} catch (unused) {
|
|
138
|
+
return String(value);
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
return String(value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Storage value → form-input string (AGL-179): the inverse of
|
|
146
|
+
* `coerceDocumentValues` for populating type-appropriate inputs
|
|
147
|
+
* (timestamps as `datetime-local` values, coordinates as "lat, lon").
|
|
148
|
+
*/ export function datasetValueToInput(field, value) {
|
|
149
|
+
if (value == null) return '';
|
|
150
|
+
if (field.type === 'timestamp' && isFiniteNumber(value)) {
|
|
151
|
+
return new Date(value).toISOString().slice(0, 16);
|
|
152
|
+
}
|
|
153
|
+
if (field.type === 'bool') {
|
|
154
|
+
return value === true ? 'true' : value === false ? 'false' : String(value);
|
|
155
|
+
}
|
|
156
|
+
if (field.type === 'coordinates' || field.type === 'sorted') {
|
|
157
|
+
return formatDatasetValue(field, value);
|
|
158
|
+
}
|
|
159
|
+
if (field.type === 'map') return formatDatasetValue(field, value);
|
|
160
|
+
return String(value);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A dataset record's FILTER TOKENS: every word-level question the records
|
|
164
|
+
* table can ask of a record, flattened into one array of strings Firestore
|
|
165
|
+
* indexes on its own. Its twin, {@link datasetFilterValues}, holds the
|
|
166
|
+
* equalities.
|
|
167
|
+
*
|
|
168
|
+
* ## Why these fields, and not the values
|
|
169
|
+
*
|
|
170
|
+
* A record's fields live under `values`, a user-defined, unbounded map that
|
|
171
|
+
* the index configuration exempts from indexing — auto-indexing it can exceed
|
|
172
|
+
* Firestore's per-document index-entry limit — so `where('values.<field>', …)`
|
|
173
|
+
* is refused. A composite index per dataset would need an admin call per
|
|
174
|
+
* dataset, runs into the project's composite-index cap, and is deleted by the
|
|
175
|
+
* next `firestore:indexes` deploy of a file that does not list it.
|
|
176
|
+
*
|
|
177
|
+
* `filterKeys` and `filterValues` need none of that. Firestore's AUTOMATIC
|
|
178
|
+
* single-field indexes serve `where('filterKeys', 'array-contains', token)`
|
|
179
|
+
* and `where('filterValues.<field>', '==', value)`, and every automatic index
|
|
180
|
+
* ends in the document name — so any number of those equalities and ONE
|
|
181
|
+
* `array-contains`, under `orderBy(documentId())`, compose by index merging,
|
|
182
|
+
* with no composite index and no index deploy, on any dataset of any project
|
|
183
|
+
* (AGL-3321).
|
|
184
|
+
*
|
|
185
|
+
* ## The token grammar
|
|
186
|
+
*
|
|
187
|
+
* `f:<fieldId>=<value>` a field equals a value. Enum (a text field with
|
|
188
|
+
* `validation.options`) values are kept exactly;
|
|
189
|
+
* plain text is lower-cased, trimmed and clipped to
|
|
190
|
+
* {@link DATASET_FILTER_VALUE_MAX} characters;
|
|
191
|
+
* booleans are `true`/`false`; numbers are
|
|
192
|
+
* `String(number)`; a `sorted` list contributes one
|
|
193
|
+
* token per member.
|
|
194
|
+
* `f:<fieldId>^<prefix>` a word of a plain text field starts with this.
|
|
195
|
+
* `s:<prefix>` a word of ANY text value (enums and list members
|
|
196
|
+
* included) starts with this — the quick search.
|
|
197
|
+
*
|
|
198
|
+
* Timestamps, references, maps, bytes, coordinates and nil fields carry no
|
|
199
|
+
* field tokens: a timestamp is asked in ranges, which one token cannot serve,
|
|
200
|
+
* and the rest are not asked about by value.
|
|
201
|
+
*
|
|
202
|
+
* The array is sorted, de-duplicated and capped at
|
|
203
|
+
* {@link DATASET_FILTER_KEYS_MAX}. Past the cap, the `=` tokens are kept
|
|
204
|
+
* first, then the per-field word prefixes, then the search prefixes, so a
|
|
205
|
+
* very long record loses search reach before it loses exact matches.
|
|
206
|
+
*/ export const DATASET_FILTER_KEYS_MAX = 500;
|
|
207
|
+
/** Plain text values are clipped to this many characters in an `=` token. */ export const DATASET_FILTER_VALUE_MAX = 64;
|
|
208
|
+
/** Word prefixes run from 1 to this many characters. */ export const DATASET_FILTER_PREFIX_MAX = 12;
|
|
209
|
+
/** Only the first this-many words of a value contribute prefixes. */ export const DATASET_FILTER_WORDS_MAX = 40;
|
|
210
|
+
/**
|
|
211
|
+
* The words of a text value: split on anything that is not a letter or a
|
|
212
|
+
* digit, lower-cased, at most {@link DATASET_FILTER_WORDS_MAX} of them.
|
|
213
|
+
*/ export function datasetFilterWords(text) {
|
|
214
|
+
return String(text).toLowerCase().split(RegExp("[^\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+", "u")).filter(Boolean).slice(0, DATASET_FILTER_WORDS_MAX);
|
|
215
|
+
}
|
|
216
|
+
/** A word's prefixes, 1 to {@link DATASET_FILTER_PREFIX_MAX} characters. */ function prefixes(word) {
|
|
217
|
+
const chars = Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX);
|
|
218
|
+
return chars.map((_char, at)=>chars.slice(0, at + 1).join(''));
|
|
219
|
+
}
|
|
220
|
+
/** A word clipped the way its stored prefixes are. */ function clipWord(word) {
|
|
221
|
+
return Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX).join('');
|
|
222
|
+
}
|
|
223
|
+
const isEnum = (field)=>{
|
|
224
|
+
var _ref;
|
|
225
|
+
var _field_validation_options, _field_validation;
|
|
226
|
+
return field.type === 'text' && ((_ref = (_field_validation = field.validation) == null ? void 0 : (_field_validation_options = _field_validation.options) == null ? void 0 : _field_validation_options.length) != null ? _ref : 0) > 0;
|
|
227
|
+
};
|
|
228
|
+
const isNumeric = (field)=>field.type === 'int32' || field.type === 'int64' || field.type === 'float';
|
|
229
|
+
/**
|
|
230
|
+
* A stored value as text, or null. Numbers are accepted because a form or an
|
|
231
|
+
* automation stores every value as text and an import may do the reverse.
|
|
232
|
+
*/ function asText(value) {
|
|
233
|
+
if (typeof value === 'string') return value;
|
|
234
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Plain text as its stored key — its `=` token's value and its
|
|
239
|
+
* `filterValues` entry: lower-cased, trimmed, clipped to
|
|
240
|
+
* {@link DATASET_FILTER_VALUE_MAX} characters. What the records table asks a
|
|
241
|
+
* typed value to equal.
|
|
242
|
+
*/ export function datasetFilterTextKey(value) {
|
|
243
|
+
return Array.from(value.trim().toLowerCase()).slice(0, DATASET_FILTER_VALUE_MAX).join('');
|
|
244
|
+
}
|
|
245
|
+
const textKey = datasetFilterTextKey;
|
|
246
|
+
/** A stored number (or its text form) as its `=` token's value. */ function numberKey(value) {
|
|
247
|
+
const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
|
|
248
|
+
return Number.isFinite(number) ? String(number) : null;
|
|
249
|
+
}
|
|
250
|
+
/** A stored boolean (or its text form) as its `=` token's value. */ function boolKey(value) {
|
|
251
|
+
if (typeof value === 'boolean') return String(value);
|
|
252
|
+
if (value === 'true' || value === 'false') return value;
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
/** Every field a model declares, in display order and then the rest. */ function modelFieldIds(model) {
|
|
256
|
+
var _model_order, _model_fields;
|
|
257
|
+
return [
|
|
258
|
+
...new Set([
|
|
259
|
+
...(_model_order = model.order) != null ? _model_order : [],
|
|
260
|
+
...Object.keys((_model_fields = model.fields) != null ? _model_fields : {})
|
|
261
|
+
])
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
/** The tokens described in {@link DATASET_FILTER_KEYS_MAX}'s comment. */ export function datasetFilterKeys(model, values) {
|
|
265
|
+
const exact = [];
|
|
266
|
+
const fieldWords = [];
|
|
267
|
+
const search = [];
|
|
268
|
+
const searchable = (text)=>{
|
|
269
|
+
for (const word of datasetFilterWords(text)){
|
|
270
|
+
for (const prefix of prefixes(word))search.push(`s:${prefix}`);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
for (const fieldId of modelFieldIds(model)){
|
|
274
|
+
var _model_fields;
|
|
275
|
+
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
|
|
276
|
+
const stored = values == null ? void 0 : values[fieldId];
|
|
277
|
+
if (!field || stored == null) continue;
|
|
278
|
+
if (field.type === 'text') {
|
|
279
|
+
const text = asText(stored);
|
|
280
|
+
if (text == null || !text.trim()) continue;
|
|
281
|
+
if (isEnum(field)) {
|
|
282
|
+
exact.push(`f:${fieldId}=${text}`);
|
|
283
|
+
} else {
|
|
284
|
+
exact.push(`f:${fieldId}=${textKey(text)}`);
|
|
285
|
+
for (const word of datasetFilterWords(text)){
|
|
286
|
+
for (const prefix of prefixes(word)){
|
|
287
|
+
fieldWords.push(`f:${fieldId}^${prefix}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
searchable(text);
|
|
292
|
+
} else if (field.type === 'bool') {
|
|
293
|
+
const key = boolKey(stored);
|
|
294
|
+
if (key) exact.push(`f:${fieldId}=${key}`);
|
|
295
|
+
} else if (isNumeric(field)) {
|
|
296
|
+
const key = numberKey(stored);
|
|
297
|
+
if (key) exact.push(`f:${fieldId}=${key}`);
|
|
298
|
+
} else if (field.type === 'sorted') {
|
|
299
|
+
for (const member of Array.isArray(stored) ? stored : [
|
|
300
|
+
stored
|
|
301
|
+
]){
|
|
302
|
+
var _asText;
|
|
303
|
+
const text = (_asText = asText(member)) == null ? void 0 : _asText.trim();
|
|
304
|
+
if (!text) continue;
|
|
305
|
+
exact.push(`f:${fieldId}=${text}`);
|
|
306
|
+
searchable(text);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const kept = [
|
|
311
|
+
...new Set([
|
|
312
|
+
...exact,
|
|
313
|
+
...fieldWords,
|
|
314
|
+
...search
|
|
315
|
+
])
|
|
316
|
+
].slice(0, DATASET_FILTER_KEYS_MAX);
|
|
317
|
+
return kept.sort();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Field ids a query can name as `filterValues.<id>`. A path string splits on
|
|
321
|
+
* `.`, and the SDKs refuse `~ * / [ ]` in one; the model editor allows only
|
|
322
|
+
* letters, digits and underscores, but a v1 dataset's column names were
|
|
323
|
+
* never checked. Backticks are refused too, as the escape a path would need.
|
|
324
|
+
*/ const FILTER_VALUE_ID = /^[^.~*/[\]`]+$/;
|
|
325
|
+
/**
|
|
326
|
+
* The query path of a field's `filterValues` entry, or null for a field id
|
|
327
|
+
* no path can name — such a field is stamped no entry, and the records table
|
|
328
|
+
* offers it no equality (its `contains`, served by `filterKeys`, stands).
|
|
329
|
+
*/ export function datasetFilterValuePath(fieldId) {
|
|
330
|
+
return FILTER_VALUE_ID.test(fieldId) && !/^__.*__$/.test(fieldId) ? `filterValues.${fieldId}` : null;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* A dataset record's FILTER VALUES: `{ [fieldId]: value }`, one scalar per
|
|
334
|
+
* field an equality can be asked of, so the records table puts every
|
|
335
|
+
* equality on its query (AGL-3321).
|
|
336
|
+
*
|
|
337
|
+
* enum text (`validation.options`) the value exactly, as its `=` token;
|
|
338
|
+
* plain text {@link datasetFilterTextKey};
|
|
339
|
+
* bool a boolean;
|
|
340
|
+
* int32 / int64 / float a number.
|
|
341
|
+
*
|
|
342
|
+
* A form or an automation stores every value as text, so `'19.50'` is the
|
|
343
|
+
* number 19.5 and `'true'` the boolean, as in {@link datasetFilterKeys}.
|
|
344
|
+
* Fields with no value, and timestamps, lists, references, maps, bytes,
|
|
345
|
+
* coordinates and nil fields, carry no entry: a list is asked by member
|
|
346
|
+
* (`f:<id>=<member>` in `filterKeys`), and the rest are not asked about by
|
|
347
|
+
* value.
|
|
348
|
+
*
|
|
349
|
+
* ## What it costs
|
|
350
|
+
*
|
|
351
|
+
* Unlike `values`, this map is NOT exempt from indexing — that is its whole
|
|
352
|
+
* point. Each entry is a scalar and costs two automatic index entries
|
|
353
|
+
* (ascending, descending), so a record costs at most two per model field —
|
|
354
|
+
* bounded by the model's field count (the `/v1` API caps a model at 100
|
|
355
|
+
* fields), far inside Firestore's 40,000 entries per document, where
|
|
356
|
+
* `values` was unbounded because a `map` or `sorted` value may hold anything.
|
|
357
|
+
*/ export function datasetFilterValues(model, values) {
|
|
358
|
+
const out = {};
|
|
359
|
+
for (const fieldId of modelFieldIds(model)){
|
|
360
|
+
var _model_fields;
|
|
361
|
+
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
|
|
362
|
+
const stored = values == null ? void 0 : values[fieldId];
|
|
363
|
+
if (!field || stored == null || !datasetFilterValuePath(fieldId)) continue;
|
|
364
|
+
if (field.type === 'text') {
|
|
365
|
+
const text = asText(stored);
|
|
366
|
+
if (text == null || !text.trim()) continue;
|
|
367
|
+
out[fieldId] = isEnum(field) ? text : textKey(text);
|
|
368
|
+
} else if (field.type === 'bool') {
|
|
369
|
+
const key = boolKey(stored);
|
|
370
|
+
if (key) out[fieldId] = key === 'true';
|
|
371
|
+
} else if (isNumeric(field)) {
|
|
372
|
+
const key = numberKey(stored);
|
|
373
|
+
if (key) out[fieldId] = Number(key);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The `filterKeys` token a word-level clause asks for, or null when it is
|
|
380
|
+
* not one:
|
|
381
|
+
*
|
|
382
|
+
* - plain text `contains` → the `^` token of the value's FIRST word, so a
|
|
383
|
+
* `contains` is a word-prefix match, not a mid-string one, and one word
|
|
384
|
+
* of several is asked (the records table says so);
|
|
385
|
+
* - list `contains` (and `equals`) → the member's `=` token;
|
|
386
|
+
* - enum, boolean, number and plain text `equals` → the `=` token of the
|
|
387
|
+
* value. The records table asks these of `filterValues` instead, which
|
|
388
|
+
* composes with a `contains`; the token remains what the quick search and
|
|
389
|
+
* older readers match.
|
|
390
|
+
* - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.
|
|
391
|
+
*
|
|
392
|
+
* `clause.field` is the field id. The operator names are the grid's: a
|
|
393
|
+
* select's `equals`, a number's `=`, a boolean's `is`, a list's `contains`.
|
|
394
|
+
*/ export function datasetFilterToken(model, clause) {
|
|
395
|
+
var _clause_value;
|
|
396
|
+
var _model_fields;
|
|
397
|
+
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[clause.field];
|
|
398
|
+
if (!field) return null;
|
|
399
|
+
const { op } = clause;
|
|
400
|
+
const value = String((_clause_value = clause.value) != null ? _clause_value : '');
|
|
401
|
+
if (!value.trim()) return null;
|
|
402
|
+
const equals = op === 'equals' || op === '=' || op === 'is';
|
|
403
|
+
if (field.type === 'text') {
|
|
404
|
+
if (isEnum(field)) return equals ? `f:${clause.field}=${value}` : null;
|
|
405
|
+
if (equals) return `f:${clause.field}=${textKey(value)}`;
|
|
406
|
+
if (op === 'contains') {
|
|
407
|
+
const [first] = datasetFilterWords(value);
|
|
408
|
+
return first ? `f:${clause.field}^${clipWord(first)}` : null;
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
if (field.type === 'bool') {
|
|
413
|
+
const key = boolKey(value.trim());
|
|
414
|
+
return equals && key ? `f:${clause.field}=${key}` : null;
|
|
415
|
+
}
|
|
416
|
+
if (isNumeric(field)) {
|
|
417
|
+
const key = numberKey(value);
|
|
418
|
+
return equals && key ? `f:${clause.field}=${key}` : null;
|
|
419
|
+
}
|
|
420
|
+
if (field.type === 'sorted') {
|
|
421
|
+
return equals || op === 'contains' ? `f:${clause.field}=${value.trim()}` : null;
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The quick-search token for one typed word, or null for a word with no
|
|
427
|
+
* letters or digits. A word with punctuation inside it is asked by its first
|
|
428
|
+
* run — split the search text with {@link datasetFilterWords} to ask for
|
|
429
|
+
* every run.
|
|
430
|
+
*/ export function datasetSearchToken(word) {
|
|
431
|
+
const [first] = datasetFilterWords(word);
|
|
432
|
+
return first ? `s:${clipWord(first)}` : null;
|
|
433
|
+
}
|
|
434
|
+
/** Dataset field name: starts with a letter; letters/digits/underscores. */ export const DATASET_FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
435
|
+
/**
|
|
436
|
+
* Stable field id from a human name: "Roast preference" → "roast_preference".
|
|
437
|
+
* Mirrors DATASET_FIELD_PATTERN; returns '' when nothing salvageable.
|
|
438
|
+
*/ export function slugifyDatasetFieldId(name) {
|
|
439
|
+
const slug = name.trim().toLowerCase().replace(/[\s-]+/g, '_').replace(/[^a-z0-9_]/g, '').replace(/^[0-9_]+/, '').replace(/_+$/, '');
|
|
440
|
+
return DATASET_FIELD_PATTERN.test(slug) ? slug : '';
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Default reference id for a new field (AGL-578): the `slugifyDatasetFieldId`
|
|
444
|
+
* slug, suffixed (`_2`, `_3`, …) to stay unique within the dataset. `taken`
|
|
445
|
+
* holds the ids already in use (compared case-insensitively). Returns '' when
|
|
446
|
+
* the name yields nothing valid. The user can override the result.
|
|
447
|
+
*/ export function defaultDatasetFieldId(name, taken) {
|
|
448
|
+
const base = slugifyDatasetFieldId(name);
|
|
449
|
+
if (!base) return '';
|
|
450
|
+
const used = new Set([
|
|
451
|
+
...taken
|
|
452
|
+
].map((id)=>id.toLowerCase()));
|
|
453
|
+
let candidate = base;
|
|
454
|
+
let suffix = 2;
|
|
455
|
+
while(used.has(candidate.toLowerCase()))candidate = `${base}_${suffix++}`;
|
|
456
|
+
return candidate;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Validate a user-entered reference id (AGL-578): non-empty, matches
|
|
460
|
+
* `DATASET_FIELD_PATTERN`, and unique within the dataset (`taken` = ids already
|
|
461
|
+
* used, compared case-insensitively). Returns an error message, or null when
|
|
462
|
+
* the id is usable.
|
|
463
|
+
*/ export function validateDatasetFieldId(id, taken) {
|
|
464
|
+
const trimmed = id.trim();
|
|
465
|
+
if (!trimmed) return 'A reference ID is required';
|
|
466
|
+
if (!DATASET_FIELD_PATTERN.test(trimmed)) return 'Start with a letter; use only letters, numbers, and underscores';
|
|
467
|
+
const used = new Set([
|
|
468
|
+
...taken
|
|
469
|
+
].map((existing)=>existing.toLowerCase()));
|
|
470
|
+
if (used.has(trimmed.toLowerCase())) return 'Another field already uses this reference ID';
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Parses a comma/newline separated list of HUMAN field names into
|
|
475
|
+
* {id, name} entries — "Roast preference" keeps its pretty name and gets
|
|
476
|
+
* the stable id `roast_preference` (AGL-558). Plain snake_case keys
|
|
477
|
+
* still work and pick up a humanized display name. Duplicate ids and
|
|
478
|
+
* unsalvageable entries are dropped rather than failing the set.
|
|
479
|
+
*/ export function parseDatasetFieldEntries(input) {
|
|
480
|
+
const seen = new Set();
|
|
481
|
+
const entries = [];
|
|
482
|
+
for (const raw of input.split(/[,\n]/)){
|
|
483
|
+
const trimmed = raw.trim();
|
|
484
|
+
if (!trimmed) continue;
|
|
485
|
+
const id = slugifyDatasetFieldId(trimmed);
|
|
486
|
+
if (!id || seen.has(id)) continue;
|
|
487
|
+
seen.add(id);
|
|
488
|
+
entries.push({
|
|
489
|
+
id,
|
|
490
|
+
name: DATASET_FIELD_PATTERN.test(trimmed) ? humanizeDatasetFieldId(trimmed) : trimmed
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return entries;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* The name a dataset is shown under: `displayName`, which is what every create
|
|
497
|
+
* path writes (AGL-536), then the pre-migration `name`. Blank when the document
|
|
498
|
+
* carries neither.
|
|
499
|
+
*/ export function datasetDisplayName(dataset) {
|
|
500
|
+
for (const candidate of [
|
|
501
|
+
dataset == null ? void 0 : dataset.displayName,
|
|
502
|
+
dataset == null ? void 0 : dataset.name
|
|
503
|
+
]){
|
|
504
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
505
|
+
return candidate.trim();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return '';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
//# sourceMappingURL=dataset-model-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-model-core.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 * The pure half of the dataset model (AGL-3668): the model and field types,\n * the v1 shim, the display and input forms of a stored value, and the\n * records table filter grammar (`filterKeys`/`filterValues` and the tokens a\n * clause or a search word asks for). No imports, so the native apps generate\n * from it (tools/scripts/mobile-pure-modules.json) and replay its answers.\n * `dataset-models.ts` re-exports all of it beside the validation and\n * coercion that reach the plugin registry.\n */\n\n/**\n * A human field entry from the quick creator: the stable id plus the\n * display name shown in table headers and bindings pickers (AGL-558).\n */\nexport interface DatasetFieldEntry {\n id: string\n name: string\n}\n\n/** Display fallback for raw ids: \"roast_preference\" → \"Roast preference\". */\nexport function humanizeDatasetFieldId(id: string): string {\n const words = id.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : id\n}\n\n/** dod.ts `FT.Tag` vocabulary plus `reference` (AGL-180 builds on it). */\nexport type DatasetFieldType =\n | 'bool'\n | 'bytes'\n | 'timestamp'\n | 'float'\n | 'int32'\n | 'int64'\n | 'nil'\n | 'text'\n | 'coordinates'\n | 'map'\n | 'sorted'\n | 'reference'\n\nexport const DATASET_FIELD_TYPES: DatasetFieldType[] = [\n 'text',\n 'bool',\n 'int32',\n 'int64',\n 'float',\n 'timestamp',\n 'coordinates',\n 'map',\n 'sorted',\n 'bytes',\n 'nil',\n 'reference',\n]\n\n/**\n * The types a person picks for a new field in the Schema dialog; the rest\n * exist for compatibility and stay editable on a field that already has one.\n */\nexport const DATASET_AUTHORABLE_FIELD_TYPES: DatasetFieldType[] = [\n 'text',\n 'bool',\n 'int32',\n 'float',\n 'timestamp',\n 'coordinates',\n 'sorted',\n 'reference',\n]\n\n/** Display names, mirroring dod.ts `lbl`. */\nexport const DATASET_FIELD_TYPE_LABELS: Record<DatasetFieldType, string> = {\n text: 'Text',\n bool: 'Boolean',\n int32: 'Integer',\n int64: 'Big integer',\n float: 'Number',\n timestamp: 'Date & time',\n coordinates: 'Coordinates',\n map: 'Map',\n sorted: 'List',\n bytes: 'Bytes',\n nil: 'Null',\n reference: 'Reference',\n}\n\n/**\n * Plain-JSON validation vocabulary (the dod.ts `Eval` shape, extended\n * with min/max and options, serialized flat instead of enum-keyed).\n */\nexport interface DatasetFieldValidation {\n required?: boolean\n /** Source of a RegExp applied to text values. */\n regex?: string\n /** Numbers/timestamps: value bound. Text: length bound. */\n min?: number\n max?: number\n /** Enum: value must be one of these (text fields). */\n options?: string[]\n}\n\nexport interface DatasetFieldDefinition {\n /**\n * Human-facing display label only. The stable reference id is the KEY in\n * `DatasetModel.fields` (and `order`), NOT this name — bindings resolve\n * `{{item.<fieldId>}}` against that key. The default id is derived from\n * this name once at creation but is user-overridable (AGL-578); renaming\n * the label afterwards never changes the id.\n */\n name: string\n type: DatasetFieldType\n /**\n * Plugin-declared custom type riding this base type (AGL-434) — see\n * plugin-manager/custom-fields. Unknown names degrade to the base type.\n */\n customType?: string\n /**\n * A page-address field's source (AGL-3475): the field an EMPTY address\n * fills in from, once — see `fillRecordAddresses` in `record-pages`.\n * Meaningful only on a field of the page-address custom type.\n */\n slugFrom?: string\n description?: string\n required?: boolean\n /** Default applied by editors when creating documents; never coerced. */\n default?: unknown\n validation?: DatasetFieldValidation\n /**\n * Reference fields (AGL-180): target collection + display field id.\n * `multiple` stores a `sorted` array of FKeys (small-cardinality\n * many-to-many per the dod.ts guidance); `onDelete` is the referenced-\n * document delete policy (default `setNull` — never cascade in v2).\n */\n reference?: {\n datasetId: string\n displayFieldId?: string\n multiple?: boolean\n onDelete?: 'restrict' | 'setNull'\n }\n}\n\nexport interface DatasetModel {\n fields: Record<string, DatasetFieldDefinition>\n /** Field ids in display order. */\n order: string[]\n}\n\n/**\n * Migration shim (v1 → v2): datasets created before models carry flat\n * `fields: string[]` — derive a model where every column is an optional\n * text field whose id is the column name, so the editor and AGL-103\n * bindings keep working on unmigrated docs during rollout.\n */\nexport function deriveModelFromFields(fields: string[]): DatasetModel {\n const model: DatasetModel = { fields: {}, order: [] }\n for (const name of fields) {\n // Raw snake_case ids read as titles in table headers and pickers;\n // the id stays the stable key (AGL-558).\n model.fields[name] = { name: humanizeDatasetFieldId(name), type: 'text' }\n model.order.push(name)\n }\n return model\n}\n\n/**\n * Model from quick-creator entries (AGL-558): stable slug ids with the\n * human display names preserved — \"Roast preference\" → id\n * `roast_preference`, name \"Roast preference\".\n */\nexport function modelFromFieldEntries(\n entries: readonly DatasetFieldEntry[],\n): DatasetModel {\n const model: DatasetModel = { fields: {}, order: [] }\n for (const entry of entries) {\n model.fields[entry.id] = { name: entry.name, type: 'text' }\n model.order.push(entry.id)\n }\n return model\n}\n\n/** The dataset's model, deriving one from v1 `fields` when absent. */\nexport function effectiveDatasetModel(dataset: {\n model?: DatasetModel\n fields?: string[]\n}): DatasetModel {\n if (dataset.model?.fields && dataset.model.order?.length) {\n return dataset.model\n }\n return deriveModelFromFields(dataset.fields ?? [])\n}\n\nconst isFiniteNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\n\n\n/** Storage value → grid display string (AGL-179). */\nexport function formatDatasetValue(\n field: DatasetFieldDefinition,\n value: unknown,\n): string {\n if (value == null || value === '') return ''\n switch (field.type) {\n case 'bool':\n return value === true ? '✓' : value === false ? '—' : String(value)\n case 'timestamp':\n return isFiniteNumber(value)\n ? new Date(value).toISOString().slice(0, 16).replace('T', ' ')\n : String(value)\n case 'coordinates': {\n const coordinates = value as { latitude?: number; longitude?: number }\n return isFiniteNumber(coordinates?.latitude) &&\n isFiniteNumber(coordinates?.longitude)\n ? `${coordinates.latitude}, ${coordinates.longitude}`\n : String(value)\n }\n case 'sorted':\n return Array.isArray(value) ? value.join(', ') : String(value)\n case 'map':\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n default:\n return String(value)\n }\n}\n\n/**\n * Storage value → form-input string (AGL-179): the inverse of\n * `coerceDocumentValues` for populating type-appropriate inputs\n * (timestamps as `datetime-local` values, coordinates as \"lat, lon\").\n */\nexport function datasetValueToInput(\n field: DatasetFieldDefinition,\n value: unknown,\n): string {\n if (value == null) return ''\n if (field.type === 'timestamp' && isFiniteNumber(value)) {\n return new Date(value).toISOString().slice(0, 16)\n }\n if (field.type === 'bool') {\n return value === true ? 'true' : value === false ? 'false' : String(value)\n }\n if (field.type === 'coordinates' || field.type === 'sorted') {\n return formatDatasetValue(field, value)\n }\n if (field.type === 'map') return formatDatasetValue(field, value)\n return String(value)\n}\n\n/**\n * A dataset record's FILTER TOKENS: every word-level question the records\n * table can ask of a record, flattened into one array of strings Firestore\n * indexes on its own. Its twin, {@link datasetFilterValues}, holds the\n * equalities.\n *\n * ## Why these fields, and not the values\n *\n * A record's fields live under `values`, a user-defined, unbounded map that\n * the index configuration exempts from indexing — auto-indexing it can exceed\n * Firestore's per-document index-entry limit — so `where('values.<field>', …)`\n * is refused. A composite index per dataset would need an admin call per\n * dataset, runs into the project's composite-index cap, and is deleted by the\n * next `firestore:indexes` deploy of a file that does not list it.\n *\n * `filterKeys` and `filterValues` need none of that. Firestore's AUTOMATIC\n * single-field indexes serve `where('filterKeys', 'array-contains', token)`\n * and `where('filterValues.<field>', '==', value)`, and every automatic index\n * ends in the document name — so any number of those equalities and ONE\n * `array-contains`, under `orderBy(documentId())`, compose by index merging,\n * with no composite index and no index deploy, on any dataset of any project\n * (AGL-3321).\n *\n * ## The token grammar\n *\n * `f:<fieldId>=<value>` a field equals a value. Enum (a text field with\n * `validation.options`) values are kept exactly;\n * plain text is lower-cased, trimmed and clipped to\n * {@link DATASET_FILTER_VALUE_MAX} characters;\n * booleans are `true`/`false`; numbers are\n * `String(number)`; a `sorted` list contributes one\n * token per member.\n * `f:<fieldId>^<prefix>` a word of a plain text field starts with this.\n * `s:<prefix>` a word of ANY text value (enums and list members\n * included) starts with this — the quick search.\n *\n * Timestamps, references, maps, bytes, coordinates and nil fields carry no\n * field tokens: a timestamp is asked in ranges, which one token cannot serve,\n * and the rest are not asked about by value.\n *\n * The array is sorted, de-duplicated and capped at\n * {@link DATASET_FILTER_KEYS_MAX}. Past the cap, the `=` tokens are kept\n * first, then the per-field word prefixes, then the search prefixes, so a\n * very long record loses search reach before it loses exact matches.\n */\nexport const DATASET_FILTER_KEYS_MAX = 500\n\n/** Plain text values are clipped to this many characters in an `=` token. */\nexport const DATASET_FILTER_VALUE_MAX = 64\n/** Word prefixes run from 1 to this many characters. */\nexport const DATASET_FILTER_PREFIX_MAX = 12\n/** Only the first this-many words of a value contribute prefixes. */\nexport const DATASET_FILTER_WORDS_MAX = 40\n\n/** One clause of the records table's filter: `{ field, op, value }`. */\nexport interface DatasetFilterClause {\n field: string\n op: string\n value: string\n}\n\n/**\n * The words of a text value: split on anything that is not a letter or a\n * digit, lower-cased, at most {@link DATASET_FILTER_WORDS_MAX} of them.\n */\nexport function datasetFilterWords(text: string): string[] {\n return String(text)\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n .slice(0, DATASET_FILTER_WORDS_MAX)\n}\n\n/** A word's prefixes, 1 to {@link DATASET_FILTER_PREFIX_MAX} characters. */\nfunction prefixes(word: string): string[] {\n const chars = Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX)\n return chars.map((_char, at) => chars.slice(0, at + 1).join(''))\n}\n\n/** A word clipped the way its stored prefixes are. */\nfunction clipWord(word: string): string {\n return Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX).join('')\n}\n\nconst isEnum = (field: DatasetFieldDefinition): boolean =>\n field.type === 'text' && (field.validation?.options?.length ?? 0) > 0\n\nconst isNumeric = (field: DatasetFieldDefinition): boolean =>\n field.type === 'int32' || field.type === 'int64' || field.type === 'float'\n\n/**\n * A stored value as text, or null. Numbers are accepted because a form or an\n * automation stores every value as text and an import may do the reverse.\n */\nfunction asText(value: unknown): string | null {\n if (typeof value === 'string') return value\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n return null\n}\n\n/**\n * Plain text as its stored key — its `=` token's value and its\n * `filterValues` entry: lower-cased, trimmed, clipped to\n * {@link DATASET_FILTER_VALUE_MAX} characters. What the records table asks a\n * typed value to equal.\n */\nexport function datasetFilterTextKey(value: string): string {\n return Array.from(value.trim().toLowerCase())\n .slice(0, DATASET_FILTER_VALUE_MAX)\n .join('')\n}\nconst textKey = datasetFilterTextKey\n\n/** A stored number (or its text form) as its `=` token's value. */\nfunction numberKey(value: unknown): string | null {\n const number =\n typeof value === 'number'\n ? value\n : typeof value === 'string' && value.trim() !== ''\n ? Number(value)\n : Number.NaN\n return Number.isFinite(number) ? String(number) : null\n}\n\n/** A stored boolean (or its text form) as its `=` token's value. */\nfunction boolKey(value: unknown): string | null {\n if (typeof value === 'boolean') return String(value)\n if (value === 'true' || value === 'false') return value\n return null\n}\n\n/** Every field a model declares, in display order and then the rest. */\nfunction modelFieldIds(model: DatasetModel): string[] {\n return [...new Set([...(model.order ?? []), ...Object.keys(model.fields ?? {})])]\n}\n\n/** The tokens described in {@link DATASET_FILTER_KEYS_MAX}'s comment. */\nexport function datasetFilterKeys(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n): string[] {\n const exact: string[] = []\n const fieldWords: string[] = []\n const search: string[] = []\n const searchable = (text: string) => {\n for (const word of datasetFilterWords(text)) {\n for (const prefix of prefixes(word)) search.push(`s:${prefix}`)\n }\n }\n for (const fieldId of modelFieldIds(model)) {\n const field = model.fields?.[fieldId]\n const stored = values?.[fieldId]\n if (!field || stored == null) continue\n if (field.type === 'text') {\n const text = asText(stored)\n if (text == null || !text.trim()) continue\n if (isEnum(field)) {\n exact.push(`f:${fieldId}=${text}`)\n } else {\n exact.push(`f:${fieldId}=${textKey(text)}`)\n for (const word of datasetFilterWords(text)) {\n for (const prefix of prefixes(word)) {\n fieldWords.push(`f:${fieldId}^${prefix}`)\n }\n }\n }\n searchable(text)\n } else if (field.type === 'bool') {\n const key = boolKey(stored)\n if (key) exact.push(`f:${fieldId}=${key}`)\n } else if (isNumeric(field)) {\n const key = numberKey(stored)\n if (key) exact.push(`f:${fieldId}=${key}`)\n } else if (field.type === 'sorted') {\n for (const member of Array.isArray(stored) ? stored : [stored]) {\n const text = asText(member)?.trim()\n if (!text) continue\n exact.push(`f:${fieldId}=${text}`)\n searchable(text)\n }\n }\n }\n const kept = [...new Set([...exact, ...fieldWords, ...search])].slice(\n 0,\n DATASET_FILTER_KEYS_MAX,\n )\n return kept.sort()\n}\n\n/** A record's `filterValues`: field id → the value an equality asks for. */\nexport type DatasetFilterValues = Record<string, string | number | boolean>\n\n/**\n * Field ids a query can name as `filterValues.<id>`. A path string splits on\n * `.`, and the SDKs refuse `~ * / [ ]` in one; the model editor allows only\n * letters, digits and underscores, but a v1 dataset's column names were\n * never checked. Backticks are refused too, as the escape a path would need.\n */\nconst FILTER_VALUE_ID = /^[^.~*/[\\]`]+$/\n\n/**\n * The query path of a field's `filterValues` entry, or null for a field id\n * no path can name — such a field is stamped no entry, and the records table\n * offers it no equality (its `contains`, served by `filterKeys`, stands).\n */\nexport function datasetFilterValuePath(fieldId: string): string | null {\n return FILTER_VALUE_ID.test(fieldId) && !/^__.*__$/.test(fieldId)\n ? `filterValues.${fieldId}`\n : null\n}\n\n/**\n * A dataset record's FILTER VALUES: `{ [fieldId]: value }`, one scalar per\n * field an equality can be asked of, so the records table puts every\n * equality on its query (AGL-3321).\n *\n * enum text (`validation.options`) the value exactly, as its `=` token;\n * plain text {@link datasetFilterTextKey};\n * bool a boolean;\n * int32 / int64 / float a number.\n *\n * A form or an automation stores every value as text, so `'19.50'` is the\n * number 19.5 and `'true'` the boolean, as in {@link datasetFilterKeys}.\n * Fields with no value, and timestamps, lists, references, maps, bytes,\n * coordinates and nil fields, carry no entry: a list is asked by member\n * (`f:<id>=<member>` in `filterKeys`), and the rest are not asked about by\n * value.\n *\n * ## What it costs\n *\n * Unlike `values`, this map is NOT exempt from indexing — that is its whole\n * point. Each entry is a scalar and costs two automatic index entries\n * (ascending, descending), so a record costs at most two per model field —\n * bounded by the model's field count (the `/v1` API caps a model at 100\n * fields), far inside Firestore's 40,000 entries per document, where\n * `values` was unbounded because a `map` or `sorted` value may hold anything.\n */\nexport function datasetFilterValues(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n): DatasetFilterValues {\n const out: DatasetFilterValues = {}\n for (const fieldId of modelFieldIds(model)) {\n const field = model.fields?.[fieldId]\n const stored = values?.[fieldId]\n if (!field || stored == null || !datasetFilterValuePath(fieldId)) continue\n if (field.type === 'text') {\n const text = asText(stored)\n if (text == null || !text.trim()) continue\n out[fieldId] = isEnum(field) ? text : textKey(text)\n } else if (field.type === 'bool') {\n const key = boolKey(stored)\n if (key) out[fieldId] = key === 'true'\n } else if (isNumeric(field)) {\n const key = numberKey(stored)\n if (key) out[fieldId] = Number(key)\n }\n }\n return out\n}\n\n/**\n * The `filterKeys` token a word-level clause asks for, or null when it is\n * not one:\n *\n * - plain text `contains` → the `^` token of the value's FIRST word, so a\n * `contains` is a word-prefix match, not a mid-string one, and one word\n * of several is asked (the records table says so);\n * - list `contains` (and `equals`) → the member's `=` token;\n * - enum, boolean, number and plain text `equals` → the `=` token of the\n * value. The records table asks these of `filterValues` instead, which\n * composes with a `contains`; the token remains what the quick search and\n * older readers match.\n * - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.\n *\n * `clause.field` is the field id. The operator names are the grid's: a\n * select's `equals`, a number's `=`, a boolean's `is`, a list's `contains`.\n */\nexport function datasetFilterToken(\n model: DatasetModel,\n clause: DatasetFilterClause,\n): string | null {\n const field = model.fields?.[clause.field]\n if (!field) return null\n const { op } = clause\n const value = String(clause.value ?? '')\n if (!value.trim()) return null\n const equals = op === 'equals' || op === '=' || op === 'is'\n if (field.type === 'text') {\n if (isEnum(field)) return equals ? `f:${clause.field}=${value}` : null\n if (equals) return `f:${clause.field}=${textKey(value)}`\n if (op === 'contains') {\n const [first] = datasetFilterWords(value)\n return first ? `f:${clause.field}^${clipWord(first)}` : null\n }\n return null\n }\n if (field.type === 'bool') {\n const key = boolKey(value.trim())\n return equals && key ? `f:${clause.field}=${key}` : null\n }\n if (isNumeric(field)) {\n const key = numberKey(value)\n return equals && key ? `f:${clause.field}=${key}` : null\n }\n if (field.type === 'sorted') {\n return equals || op === 'contains' ? `f:${clause.field}=${value.trim()}` : null\n }\n return null\n}\n\n/**\n * The quick-search token for one typed word, or null for a word with no\n * letters or digits. A word with punctuation inside it is asked by its first\n * run — split the search text with {@link datasetFilterWords} to ask for\n * every run.\n */\nexport function datasetSearchToken(word: string): string | null {\n const [first] = datasetFilterWords(word)\n return first ? `s:${clipWord(first)}` : null\n}\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\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 * 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"],"names":["humanizeDatasetFieldId","id","words","replace","trim","charAt","toUpperCase","slice","DATASET_FIELD_TYPES","DATASET_AUTHORABLE_FIELD_TYPES","DATASET_FIELD_TYPE_LABELS","text","bool","int32","int64","float","timestamp","coordinates","map","sorted","bytes","nil","reference","deriveModelFromFields","fields","model","order","name","type","push","modelFromFieldEntries","entries","entry","effectiveDatasetModel","dataset","length","isFiniteNumber","value","Number","isFinite","formatDatasetValue","field","String","Date","toISOString","latitude","longitude","Array","isArray","join","JSON","stringify","datasetValueToInput","DATASET_FILTER_KEYS_MAX","DATASET_FILTER_VALUE_MAX","DATASET_FILTER_PREFIX_MAX","DATASET_FILTER_WORDS_MAX","datasetFilterWords","toLowerCase","split","filter","Boolean","prefixes","word","chars","from","_char","at","clipWord","isEnum","validation","options","isNumeric","asText","datasetFilterTextKey","textKey","numberKey","number","NaN","boolKey","modelFieldIds","Set","Object","keys","datasetFilterKeys","values","exact","fieldWords","search","searchable","prefix","fieldId","stored","key","member","kept","sort","FILTER_VALUE_ID","datasetFilterValuePath","test","datasetFilterValues","out","datasetFilterToken","clause","op","equals","first","datasetSearchToken","DATASET_FIELD_PATTERN","slugifyDatasetFieldId","slug","defaultDatasetFieldId","taken","base","used","candidate","suffix","has","validateDatasetFieldId","trimmed","existing","parseDatasetFieldEntries","input","seen","raw","add","datasetDisplayName","displayName"],"mappings":"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"}
|