@aglyn/plugins-data 1.0.0-beta.220 → 1.0.0-beta.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/package.json +10 -10
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
- package/src/lib/artifact/dataset-schema.d.ts +136 -0
- package/src/lib/artifact/dataset-schema.js +232 -0
- package/src/lib/artifact/dataset-schema.js.map +1 -0
- package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-record-dialog.component.js +1 -1
- package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
- package/src/lib/components/dataset-record-filter.d.ts +1 -1
- package/src/lib/components/dataset-record-filter.js +2 -2
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +55 -23
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +91 -403
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +12 -0
- package/src/lib/constants/bundle-common.js +13 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +16 -1
- package/src/lib/declarations.console-server.js +34 -2
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.server.d.ts +13 -0
- package/src/lib/declarations.server.js +48 -2
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
- package/src/lib/model/dataset-models.d.ts +392 -0
- package/src/lib/model/dataset-models.js +806 -0
- package/src/lib/model/dataset-models.js.map +1 -0
- package/src/lib/model/dataset-record-view.d.ts +1 -1
- package/src/lib/model/dataset-record-view.js +1 -2
- package/src/lib/model/dataset-record-view.js.map +1 -1
- package/src/lib/model/dataset-record.js +2 -2
- package/src/lib/model/dataset-record.js.map +1 -1
- package/src/lib/model/datasets.d.ts +162 -0
- package/src/lib/model/datasets.js +227 -0
- package/src/lib/model/datasets.js.map +1 -0
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +40 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
- package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
- package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
- package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
- package/src/lib/record-pages/record-page-read.server.js +175 -0
- package/src/lib/record-pages/record-page-read.server.js.map +1 -0
- package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
- package/src/lib/record-pages/record-page-resolver.server.js +131 -0
- package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
- package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
- package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
- package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
- package/src/lib/record-pages/record-page-source.d.ts +42 -0
- package/src/lib/record-pages/record-page-source.js +176 -0
- package/src/lib/record-pages/record-page-source.js.map +1 -0
- package/src/lib/record-pages/record-pages-route.d.ts +42 -0
- package/src/lib/record-pages/record-pages-route.js +348 -0
- package/src/lib/record-pages/record-pages-route.js.map +1 -0
- package/src/lib/record-pages/record-pages.d.ts +218 -0
- package/src/lib/record-pages/record-pages.js +351 -0
- package/src/lib/record-pages/record-pages.js.map +1 -0
- package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
- package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
- package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
- package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
- package/src/lib/record-pages/record-template-section.component.js +491 -0
- package/src/lib/record-pages/record-template-section.component.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
- package/src/lib/repeat/dataset-repeat-rows.js +30 -12
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
- package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat/repeat-record-pages.js +64 -0
- package/src/lib/repeat/repeat-record-pages.js.map +1 -0
- package/src/lib/server/announce-dataset-records.d.ts +1 -1
- package/src/lib/server/announce-dataset-records.js +1 -1
- package/src/lib/server/announce-dataset-records.js.map +1 -1
- package/src/lib/server/api-v1/datasets.js +5 -3
- package/src/lib/server/api-v1/datasets.js.map +1 -1
- package/src/lib/server/dataset-figures.js +2 -2
- package/src/lib/server/dataset-figures.js.map +1 -1
- package/src/lib/server/dataset-live-pages.d.ts +147 -0
- package/src/lib/server/dataset-live-pages.js +436 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dataset-steps.server.d.ts +45 -0
- package/src/lib/server/dataset-steps.server.js +268 -0
- package/src/lib/server/dataset-steps.server.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +2 -0
- package/src/lib/server/datasets-route.js +102 -6
- package/src/lib/server/datasets-route.js.map +1 -1
- package/src/lib/server/resolve-dataset.d.ts +40 -0
- package/src/lib/server/resolve-dataset.js +64 -0
- package/src/lib/server/resolve-dataset.js.map +1 -0
- package/src/lib/server.d.ts +3 -2
- package/src/lib/server.js +8 -9
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-bundle/datasets-package.d.ts +42 -0
- package/src/lib/site-bundle/datasets-package.js +95 -0
- package/src/lib/site-bundle/datasets-package.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
- package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
- package/src/lib/tenant-api.js +30 -0
- package/src/lib/tenant-api.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
- package/src/lib/transfer/dataset-transfer-key.js +31 -0
- package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
- package/src/lib/transfer/dataset-transfer-model.js +536 -0
- package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
- package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
- package/src/lib/transfer/dataset-transfer-resource.js +41 -0
- package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
- package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
- package/src/lib/transfer/dataset-transfer.server.js +930 -0
- package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +0 -36
- package/src/lib/model/dataset-csv.js +0 -85
- package/src/lib/model/dataset-csv.js.map +0 -1
- package/src/lib/model/dataset-io.d.ts +0 -44
- package/src/lib/model/dataset-io.js +0 -85
- package/src/lib/model/dataset-io.js.map +0 -1
- package/src/lib/model/index.js.map +0 -1
- package/src/lib/server/datasets-export-route.d.ts +0 -57
- package/src/lib/server/datasets-export-route.js +0 -229
- package/src/lib/server/datasets-export-route.js.map +0 -1
|
@@ -0,0 +1,392 @@
|
|
|
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 { RepeatRowsModel } from '@aglyn/aglyn/app-utils/expand-repeatables';
|
|
18
|
+
/**
|
|
19
|
+
* Dataset models (AGL-177): the runtime promotion of the type-level
|
|
20
|
+
* blueprint in `libs/shared/data/types/src/lib/dod.ts` (kept as the
|
|
21
|
+
* referenced design doc — this module is the single runtime source of
|
|
22
|
+
* truth). A model lives on the dataset doc
|
|
23
|
+
* (`orgs/{orgId}/datasets/{id}.model`) and drives the typed editor, import
|
|
24
|
+
* and site-restore validation, and every record write — the console, `/v1`,
|
|
25
|
+
* a bound form's submission and an automation step — through the shared
|
|
26
|
+
* `validateDocument`/`coerceDocumentValues` pair so they can't disagree. The
|
|
27
|
+
* form and the automation step reach the pair through
|
|
28
|
+
* `prepareDatasetRecordWrite` (AGL-2773).
|
|
29
|
+
*
|
|
30
|
+
* Storage conventions (Firestore-safe): timestamps as epoch millis
|
|
31
|
+
* numbers, coordinates as `{ latitude, longitude }`, `sorted` as arrays,
|
|
32
|
+
* `map` as plain objects, references as target-document id strings.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* A human field entry from the quick creator: the stable id plus the
|
|
36
|
+
* display name shown in table headers and bindings pickers (AGL-558).
|
|
37
|
+
*/
|
|
38
|
+
export interface DatasetFieldEntry {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
/** Display fallback for raw ids: "roast_preference" → "Roast preference". */
|
|
43
|
+
export declare function humanizeDatasetFieldId(id: string): string;
|
|
44
|
+
/** dod.ts `FT.Tag` vocabulary plus `reference` (AGL-180 builds on it). */
|
|
45
|
+
export type DatasetFieldType = 'bool' | 'bytes' | 'timestamp' | 'float' | 'int32' | 'int64' | 'nil' | 'text' | 'coordinates' | 'map' | 'sorted' | 'reference';
|
|
46
|
+
export declare const DATASET_FIELD_TYPES: DatasetFieldType[];
|
|
47
|
+
/** Display names, mirroring dod.ts `lbl`. */
|
|
48
|
+
export declare const DATASET_FIELD_TYPE_LABELS: Record<DatasetFieldType, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Plain-JSON validation vocabulary (the dod.ts `Eval` shape, extended
|
|
51
|
+
* with min/max and options, serialized flat instead of enum-keyed).
|
|
52
|
+
*/
|
|
53
|
+
export interface DatasetFieldValidation {
|
|
54
|
+
required?: boolean;
|
|
55
|
+
/** Source of a RegExp applied to text values. */
|
|
56
|
+
regex?: string;
|
|
57
|
+
/** Numbers/timestamps: value bound. Text: length bound. */
|
|
58
|
+
min?: number;
|
|
59
|
+
max?: number;
|
|
60
|
+
/** Enum: value must be one of these (text fields). */
|
|
61
|
+
options?: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface DatasetFieldDefinition {
|
|
64
|
+
/**
|
|
65
|
+
* Human-facing display label only. The stable reference id is the KEY in
|
|
66
|
+
* `DatasetModel.fields` (and `order`), NOT this name — bindings resolve
|
|
67
|
+
* `{{item.<fieldId>}}` against that key. The default id is derived from
|
|
68
|
+
* this name once at creation but is user-overridable (AGL-578); renaming
|
|
69
|
+
* the label afterwards never changes the id.
|
|
70
|
+
*/
|
|
71
|
+
name: string;
|
|
72
|
+
type: DatasetFieldType;
|
|
73
|
+
/**
|
|
74
|
+
* Plugin-declared custom type riding this base type (AGL-434) — see
|
|
75
|
+
* plugin-manager/custom-fields. Unknown names degrade to the base type.
|
|
76
|
+
*/
|
|
77
|
+
customType?: string;
|
|
78
|
+
/**
|
|
79
|
+
* A page-address field's source (AGL-3475): the field an EMPTY address
|
|
80
|
+
* fills in from, once — see `fillRecordAddresses` in `record-pages`.
|
|
81
|
+
* Meaningful only on a field of the page-address custom type.
|
|
82
|
+
*/
|
|
83
|
+
slugFrom?: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
required?: boolean;
|
|
86
|
+
/** Default applied by editors when creating documents; never coerced. */
|
|
87
|
+
default?: unknown;
|
|
88
|
+
validation?: DatasetFieldValidation;
|
|
89
|
+
/**
|
|
90
|
+
* Reference fields (AGL-180): target collection + display field id.
|
|
91
|
+
* `multiple` stores a `sorted` array of FKeys (small-cardinality
|
|
92
|
+
* many-to-many per the dod.ts guidance); `onDelete` is the referenced-
|
|
93
|
+
* document delete policy (default `setNull` — never cascade in v2).
|
|
94
|
+
*/
|
|
95
|
+
reference?: {
|
|
96
|
+
datasetId: string;
|
|
97
|
+
displayFieldId?: string;
|
|
98
|
+
multiple?: boolean;
|
|
99
|
+
onDelete?: 'restrict' | 'setNull';
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export interface DatasetModel {
|
|
103
|
+
fields: Record<string, DatasetFieldDefinition>;
|
|
104
|
+
/** Field ids in display order. */
|
|
105
|
+
order: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Migration shim (v1 → v2): datasets created before models carry flat
|
|
109
|
+
* `fields: string[]` — derive a model where every column is an optional
|
|
110
|
+
* text field whose id is the column name, so the editor and AGL-103
|
|
111
|
+
* bindings keep working on unmigrated docs during rollout.
|
|
112
|
+
*/
|
|
113
|
+
export declare function deriveModelFromFields(fields: string[]): DatasetModel;
|
|
114
|
+
/**
|
|
115
|
+
* Model from quick-creator entries (AGL-558): stable slug ids with the
|
|
116
|
+
* human display names preserved — "Roast preference" → id
|
|
117
|
+
* `roast_preference`, name "Roast preference".
|
|
118
|
+
*/
|
|
119
|
+
export declare function modelFromFieldEntries(entries: readonly DatasetFieldEntry[]): DatasetModel;
|
|
120
|
+
/**
|
|
121
|
+
* The part of a dataset's model a repeat reads (AGL-180): each reference
|
|
122
|
+
* field, by the id of the dataset it points into, which is a key the rows map
|
|
123
|
+
* answers under. Every field the model declares, in `order` or not — a hop
|
|
124
|
+
* resolves wherever its target's rows were loaded.
|
|
125
|
+
*/
|
|
126
|
+
export declare function repeatRowsModelOf(model: DatasetModel): RepeatRowsModel;
|
|
127
|
+
/** The dataset's model, deriving one from v1 `fields` when absent. */
|
|
128
|
+
export declare function effectiveDatasetModel(dataset: {
|
|
129
|
+
model?: DatasetModel;
|
|
130
|
+
fields?: string[];
|
|
131
|
+
}): DatasetModel;
|
|
132
|
+
/**
|
|
133
|
+
* Validates a document's values against a model, returning fieldId →
|
|
134
|
+
* human-readable error (empty object = valid). Values are expected in
|
|
135
|
+
* storage form (see the module doc); use `coerceDocumentValues` first
|
|
136
|
+
* when starting from user input strings.
|
|
137
|
+
*/
|
|
138
|
+
export declare function validateDocument(model: DatasetModel, values: Record<string, unknown>): Record<string, string>;
|
|
139
|
+
/** Storage value → grid display string (AGL-179). */
|
|
140
|
+
export declare function formatDatasetValue(field: DatasetFieldDefinition, value: unknown): string;
|
|
141
|
+
/**
|
|
142
|
+
* Storage value → form-input string (AGL-179): the inverse of
|
|
143
|
+
* `coerceDocumentValues` for populating type-appropriate inputs
|
|
144
|
+
* (timestamps as `datetime-local` values, coordinates as "lat, lon").
|
|
145
|
+
*/
|
|
146
|
+
export declare function datasetValueToInput(field: DatasetFieldDefinition, value: unknown): string;
|
|
147
|
+
/**
|
|
148
|
+
* Whether a field stores a list: `sorted`, and a `reference` that may point at
|
|
149
|
+
* more than one record. Both go through {@link coerceListValue}.
|
|
150
|
+
*/
|
|
151
|
+
export declare function isListField(field: DatasetFieldDefinition): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* A list field's value in storage form (AGL-3496) — the ONE coercion every
|
|
154
|
+
* writer of a `sorted` or multiple-`reference` field reaches through
|
|
155
|
+
* {@link coerceDocumentValues}: the console's record form and its CSV/JSON
|
|
156
|
+
* import, `/api/orgs/datasets`, `/v1` record create and update, form
|
|
157
|
+
* submissions and workflow steps (`prepareDatasetRecordWrite`).
|
|
158
|
+
*
|
|
159
|
+
* - An array is taken as the list: entries stringified and trimmed, empty
|
|
160
|
+
* ones dropped.
|
|
161
|
+
* - A string that parses as a JSON array of scalars is that array. A JSON
|
|
162
|
+
* import hands a list cell over as `["Residential","Commercial"]`, and so
|
|
163
|
+
* does any API client that serialized one; split on commas, it was stored
|
|
164
|
+
* as `["[\"Residential\"", "\"Commercial\"]"]` and no filter matched it.
|
|
165
|
+
* The console form round-trips such a record too: it shows the stored
|
|
166
|
+
* fragments joined, which reads back as the JSON array, so re-saving a
|
|
167
|
+
* record written that way repairs it.
|
|
168
|
+
* - Any other string is comma-separated, as typed into the form.
|
|
169
|
+
* - Anything else (a number, an object, an array holding objects) passes
|
|
170
|
+
* through untouched for `validateDocument` to refuse.
|
|
171
|
+
*/
|
|
172
|
+
export declare function coerceListValue(raw: unknown): unknown;
|
|
173
|
+
/**
|
|
174
|
+
* Coerces user-input strings (form fields, CSV cells) into storage form
|
|
175
|
+
* per the field type. Unparseable input is passed through untouched so
|
|
176
|
+
* `validateDocument` reports it instead of silently mangling it.
|
|
177
|
+
*
|
|
178
|
+
* A list field is coerced whatever shape it arrives in — array or string —
|
|
179
|
+
* through {@link coerceListValue}; every other non-string value is already
|
|
180
|
+
* in storage form and passes through.
|
|
181
|
+
*/
|
|
182
|
+
export declare function coerceDocumentValues(model: DatasetModel, input: Record<string, unknown>): Record<string, unknown>;
|
|
183
|
+
/**
|
|
184
|
+
* Every reference id a record holds, flattened out of its `reference` fields
|
|
185
|
+
* into one de-duplicated list.
|
|
186
|
+
*
|
|
187
|
+
* A reference field stores either a single target id or — when
|
|
188
|
+
* `reference.multiple` is set — an array of them, and a model may declare
|
|
189
|
+
* several. Both shapes flatten into the same list because the question asked
|
|
190
|
+
* of it is never "which field", it is "does this record still point at that
|
|
191
|
+
* document".
|
|
192
|
+
*
|
|
193
|
+
* Fields are read from `order` UNIONED with the keys of `fields`. Every other
|
|
194
|
+
* pass over a model walks `order` alone, which is right for anything the user
|
|
195
|
+
* sees — but a reference declared without a display slot still holds a real
|
|
196
|
+
* FKey, and an integrity index that skipped it would under-report.
|
|
197
|
+
*/
|
|
198
|
+
export declare function datasetReferencedIds(model: DatasetModel, values: Record<string, unknown> | undefined): string[];
|
|
199
|
+
/**
|
|
200
|
+
* The integrity index a record write carries, so referential integrity can be
|
|
201
|
+
* decided by a QUERY rather than by the rows a listener happened to fetch.
|
|
202
|
+
*
|
|
203
|
+
* Deleting a record has to answer "is anything still pointing at this?", and
|
|
204
|
+
* `restrict` refuses the delete on a yes while `setNull` strips the FKey. That
|
|
205
|
+
* question cannot be asked where the answer is stored: dataset fields are
|
|
206
|
+
* user-defined, so `records.values` carries a deliberate index exemption in
|
|
207
|
+
* `cloud/firebase-firestore.indexes.json` — auto-indexing an unbounded map
|
|
208
|
+
* blows Firestore's per-document index-entry limit — and
|
|
209
|
+
* `where('values.<fieldId>', '==', id)` is therefore FAILED_PRECONDITION.
|
|
210
|
+
* Reading a page of records and filtering them in the browser answers only for
|
|
211
|
+
* the rows in that page, which is a browse window standing in for a
|
|
212
|
+
* correctness check: a reference held outside it reads as no reference at all,
|
|
213
|
+
* the delete proceeds, and `restrict` silently fails to restrict.
|
|
214
|
+
*
|
|
215
|
+
* `referencedIds` is that same reference, denormalized to a top-level array
|
|
216
|
+
* Firestore will index, so one `array-contains` reaches every record.
|
|
217
|
+
*
|
|
218
|
+
* ⚠️ OMITTED when a record references nothing, rather than written as `[]`.
|
|
219
|
+
* `isNotEmpty` is served as `!= null` and an empty array is not null, so a
|
|
220
|
+
* record stamped `[]` answers "holds a reference" for a dataset that holds
|
|
221
|
+
* none.
|
|
222
|
+
*
|
|
223
|
+
* ⚠️ Spread this at EVERY write that sets a record's `values`. A write that
|
|
224
|
+
* sets values without it leaves the index describing the PREVIOUS values, and
|
|
225
|
+
* a stale integrity index is the exact failure it exists to prevent — with the
|
|
226
|
+
* UI reporting the record as safe to delete. On a merging write use
|
|
227
|
+
* `datasetIntegrityUpdate` instead: omission leaves a stale array in place
|
|
228
|
+
* where clearing the last reference has to remove the field.
|
|
229
|
+
*/
|
|
230
|
+
export declare function datasetIntegrityFields(model: DatasetModel, values: Record<string, unknown> | undefined): {
|
|
231
|
+
referencedIds?: string[];
|
|
232
|
+
filterKeys?: string[];
|
|
233
|
+
filterValues?: DatasetFilterValues;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* `datasetIntegrityFields` for a MERGING write (`update`, `set` with
|
|
237
|
+
* `mergeFields`), where leaving the field out preserves what is stored
|
|
238
|
+
* rather than clearing it.
|
|
239
|
+
*
|
|
240
|
+
* A record whose last reference is cleared has to have the field REMOVED, or
|
|
241
|
+
* it goes on answering the `array-contains` for a document it no longer points
|
|
242
|
+
* at — a `restrict` that refuses a delete nothing is holding. `clear` is the
|
|
243
|
+
* caller's delete sentinel: `deleteField()` in the web SDK,
|
|
244
|
+
* `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported
|
|
245
|
+
* because this module is shared by both and must stay SDK-free. The filter
|
|
246
|
+
* fields are cleared the same way, so a record emptied of every filterable
|
|
247
|
+
* value stops answering the filters its old values did.
|
|
248
|
+
*
|
|
249
|
+
* ⚠️ `filterValues` is a MAP, and it is written WHOLE: `update` and
|
|
250
|
+
* `mergeFields` replace a field outright, so a value cleared since the last
|
|
251
|
+
* write leaves the map with it. `set(…, { merge: true })` does NOT — it merges
|
|
252
|
+
* maps key by key and would keep the cleared value answering its old
|
|
253
|
+
* equality — so a record write never uses it; list the fields in
|
|
254
|
+
* `mergeFields` instead.
|
|
255
|
+
*/
|
|
256
|
+
export declare function datasetIntegrityUpdate<TClear>(model: DatasetModel, values: Record<string, unknown> | undefined, clear: TClear): {
|
|
257
|
+
referencedIds: string[] | TClear;
|
|
258
|
+
filterKeys: string[] | TClear;
|
|
259
|
+
filterValues: DatasetFilterValues | TClear;
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* A dataset record's FILTER TOKENS: every word-level question the records
|
|
263
|
+
* table can ask of a record, flattened into one array of strings Firestore
|
|
264
|
+
* indexes on its own. Its twin, {@link datasetFilterValues}, holds the
|
|
265
|
+
* equalities.
|
|
266
|
+
*
|
|
267
|
+
* ## Why these fields, and not the values
|
|
268
|
+
*
|
|
269
|
+
* A record's fields live under `values`, a user-defined, unbounded map that
|
|
270
|
+
* the index configuration exempts from indexing — auto-indexing it can exceed
|
|
271
|
+
* Firestore's per-document index-entry limit — so `where('values.<field>', …)`
|
|
272
|
+
* is refused. A composite index per dataset would need an admin call per
|
|
273
|
+
* dataset, runs into the project's composite-index cap, and is deleted by the
|
|
274
|
+
* next `firestore:indexes` deploy of a file that does not list it.
|
|
275
|
+
*
|
|
276
|
+
* `filterKeys` and `filterValues` need none of that. Firestore's AUTOMATIC
|
|
277
|
+
* single-field indexes serve `where('filterKeys', 'array-contains', token)`
|
|
278
|
+
* and `where('filterValues.<field>', '==', value)`, and every automatic index
|
|
279
|
+
* ends in the document name — so any number of those equalities and ONE
|
|
280
|
+
* `array-contains`, under `orderBy(documentId())`, compose by index merging,
|
|
281
|
+
* with no composite index and no index deploy, on any dataset of any project
|
|
282
|
+
* (AGL-3321).
|
|
283
|
+
*
|
|
284
|
+
* ## The token grammar
|
|
285
|
+
*
|
|
286
|
+
* `f:<fieldId>=<value>` a field equals a value. Enum (a text field with
|
|
287
|
+
* `validation.options`) values are kept exactly;
|
|
288
|
+
* plain text is lower-cased, trimmed and clipped to
|
|
289
|
+
* {@link DATASET_FILTER_VALUE_MAX} characters;
|
|
290
|
+
* booleans are `true`/`false`; numbers are
|
|
291
|
+
* `String(number)`; a `sorted` list contributes one
|
|
292
|
+
* token per member.
|
|
293
|
+
* `f:<fieldId>^<prefix>` a word of a plain text field starts with this.
|
|
294
|
+
* `s:<prefix>` a word of ANY text value (enums and list members
|
|
295
|
+
* included) starts with this — the quick search.
|
|
296
|
+
*
|
|
297
|
+
* Timestamps, references, maps, bytes, coordinates and nil fields carry no
|
|
298
|
+
* field tokens: a timestamp is asked in ranges, which one token cannot serve,
|
|
299
|
+
* and the rest are not asked about by value.
|
|
300
|
+
*
|
|
301
|
+
* The array is sorted, de-duplicated and capped at
|
|
302
|
+
* {@link DATASET_FILTER_KEYS_MAX}. Past the cap, the `=` tokens are kept
|
|
303
|
+
* first, then the per-field word prefixes, then the search prefixes, so a
|
|
304
|
+
* very long record loses search reach before it loses exact matches.
|
|
305
|
+
*/
|
|
306
|
+
export declare const DATASET_FILTER_KEYS_MAX = 500;
|
|
307
|
+
/** Plain text values are clipped to this many characters in an `=` token. */
|
|
308
|
+
export declare const DATASET_FILTER_VALUE_MAX = 64;
|
|
309
|
+
/** Word prefixes run from 1 to this many characters. */
|
|
310
|
+
export declare const DATASET_FILTER_PREFIX_MAX = 12;
|
|
311
|
+
/** Only the first this-many words of a value contribute prefixes. */
|
|
312
|
+
export declare const DATASET_FILTER_WORDS_MAX = 40;
|
|
313
|
+
/** One clause of the records table's filter: `{ field, op, value }`. */
|
|
314
|
+
export interface DatasetFilterClause {
|
|
315
|
+
field: string;
|
|
316
|
+
op: string;
|
|
317
|
+
value: string;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The words of a text value: split on anything that is not a letter or a
|
|
321
|
+
* digit, lower-cased, at most {@link DATASET_FILTER_WORDS_MAX} of them.
|
|
322
|
+
*/
|
|
323
|
+
export declare function datasetFilterWords(text: string): string[];
|
|
324
|
+
/**
|
|
325
|
+
* Plain text as its stored key — its `=` token's value and its
|
|
326
|
+
* `filterValues` entry: lower-cased, trimmed, clipped to
|
|
327
|
+
* {@link DATASET_FILTER_VALUE_MAX} characters. What the records table asks a
|
|
328
|
+
* typed value to equal.
|
|
329
|
+
*/
|
|
330
|
+
export declare function datasetFilterTextKey(value: string): string;
|
|
331
|
+
/** The tokens described in {@link DATASET_FILTER_KEYS_MAX}'s comment. */
|
|
332
|
+
export declare function datasetFilterKeys(model: DatasetModel, values: Record<string, unknown> | undefined): string[];
|
|
333
|
+
/** A record's `filterValues`: field id → the value an equality asks for. */
|
|
334
|
+
export type DatasetFilterValues = Record<string, string | number | boolean>;
|
|
335
|
+
/**
|
|
336
|
+
* The query path of a field's `filterValues` entry, or null for a field id
|
|
337
|
+
* no path can name — such a field is stamped no entry, and the records table
|
|
338
|
+
* offers it no equality (its `contains`, served by `filterKeys`, stands).
|
|
339
|
+
*/
|
|
340
|
+
export declare function datasetFilterValuePath(fieldId: string): string | null;
|
|
341
|
+
/**
|
|
342
|
+
* A dataset record's FILTER VALUES: `{ [fieldId]: value }`, one scalar per
|
|
343
|
+
* field an equality can be asked of, so the records table puts every
|
|
344
|
+
* equality on its query (AGL-3321).
|
|
345
|
+
*
|
|
346
|
+
* enum text (`validation.options`) the value exactly, as its `=` token;
|
|
347
|
+
* plain text {@link datasetFilterTextKey};
|
|
348
|
+
* bool a boolean;
|
|
349
|
+
* int32 / int64 / float a number.
|
|
350
|
+
*
|
|
351
|
+
* A form or an automation stores every value as text, so `'19.50'` is the
|
|
352
|
+
* number 19.5 and `'true'` the boolean, as in {@link datasetFilterKeys}.
|
|
353
|
+
* Fields with no value, and timestamps, lists, references, maps, bytes,
|
|
354
|
+
* coordinates and nil fields, carry no entry: a list is asked by member
|
|
355
|
+
* (`f:<id>=<member>` in `filterKeys`), and the rest are not asked about by
|
|
356
|
+
* value.
|
|
357
|
+
*
|
|
358
|
+
* ## What it costs
|
|
359
|
+
*
|
|
360
|
+
* Unlike `values`, this map is NOT exempt from indexing — that is its whole
|
|
361
|
+
* point. Each entry is a scalar and costs two automatic index entries
|
|
362
|
+
* (ascending, descending), so a record costs at most two per model field —
|
|
363
|
+
* bounded by the model's field count (the `/v1` API caps a model at 100
|
|
364
|
+
* fields), far inside Firestore's 40,000 entries per document, where
|
|
365
|
+
* `values` was unbounded because a `map` or `sorted` value may hold anything.
|
|
366
|
+
*/
|
|
367
|
+
export declare function datasetFilterValues(model: DatasetModel, values: Record<string, unknown> | undefined): DatasetFilterValues;
|
|
368
|
+
/**
|
|
369
|
+
* The `filterKeys` token a word-level clause asks for, or null when it is
|
|
370
|
+
* not one:
|
|
371
|
+
*
|
|
372
|
+
* - plain text `contains` → the `^` token of the value's FIRST word, so a
|
|
373
|
+
* `contains` is a word-prefix match, not a mid-string one, and one word
|
|
374
|
+
* of several is asked (the records table says so);
|
|
375
|
+
* - list `contains` (and `equals`) → the member's `=` token;
|
|
376
|
+
* - enum, boolean, number and plain text `equals` → the `=` token of the
|
|
377
|
+
* value. The records table asks these of `filterValues` instead, which
|
|
378
|
+
* composes with a `contains`; the token remains what the quick search and
|
|
379
|
+
* older readers match.
|
|
380
|
+
* - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.
|
|
381
|
+
*
|
|
382
|
+
* `clause.field` is the field id. The operator names are the grid's: a
|
|
383
|
+
* select's `equals`, a number's `=`, a boolean's `is`, a list's `contains`.
|
|
384
|
+
*/
|
|
385
|
+
export declare function datasetFilterToken(model: DatasetModel, clause: DatasetFilterClause): string | null;
|
|
386
|
+
/**
|
|
387
|
+
* The quick-search token for one typed word, or null for a word with no
|
|
388
|
+
* letters or digits. A word with punctuation inside it is asked by its first
|
|
389
|
+
* run — split the search text with {@link datasetFilterWords} to ask for
|
|
390
|
+
* every run.
|
|
391
|
+
*/
|
|
392
|
+
export declare function datasetSearchToken(word: string): string | null;
|