@aglyn/aglyn 1.0.0-beta.198 → 1.0.0-beta.200

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.
Files changed (113) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-search.d.ts +39 -0
  3. package/src/lib/app-utils/activity-search.js +85 -0
  4. package/src/lib/app-utils/activity-search.js.map +1 -0
  5. package/src/lib/app-utils/admin-audit-index.d.ts +104 -0
  6. package/src/lib/app-utils/admin-audit-index.js +177 -0
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -0
  8. package/src/lib/app-utils/artifact-list-keys.d.ts +51 -0
  9. package/src/lib/app-utils/artifact-list-keys.js +144 -0
  10. package/src/lib/app-utils/artifact-list-keys.js.map +1 -0
  11. package/src/lib/app-utils/content-query-fields.d.ts +69 -0
  12. package/src/lib/app-utils/content-query-fields.js +79 -0
  13. package/src/lib/app-utils/content-query-fields.js.map +1 -0
  14. package/src/lib/app-utils/crm.d.ts +151 -1
  15. package/src/lib/app-utils/crm.js +388 -1
  16. package/src/lib/app-utils/crm.js.map +1 -1
  17. package/src/lib/app-utils/dataset-models.d.ts +75 -24
  18. package/src/lib/app-utils/dataset-models.js +107 -40
  19. package/src/lib/app-utils/dataset-models.js.map +1 -1
  20. package/src/lib/app-utils/docs-index.generated.js +40 -40
  21. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  22. package/src/lib/app-utils/email-search.d.ts +36 -0
  23. package/src/lib/app-utils/email-search.js +53 -0
  24. package/src/lib/app-utils/email-search.js.map +1 -0
  25. package/src/lib/app-utils/forms.d.ts +83 -3
  26. package/src/lib/app-utils/forms.js +67 -0
  27. package/src/lib/app-utils/forms.js.map +1 -1
  28. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +7 -6
  29. package/src/lib/app-utils/marketplace-listing-visibility.js +12 -10
  30. package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
  31. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  32. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  33. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  35. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  36. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  38. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  39. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  41. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  42. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  44. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  45. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  47. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  48. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  50. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  51. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  53. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  54. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  56. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  57. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  59. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  60. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  62. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  63. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  65. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  66. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  68. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  69. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  70. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  71. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  72. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  73. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  74. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  75. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  76. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  77. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  78. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  79. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  80. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  81. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  82. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  83. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  84. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  85. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  86. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  87. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  88. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  89. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  90. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  91. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  92. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  93. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  94. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  95. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  96. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  97. package/src/lib/app-utils/media-metadata.d.ts +79 -0
  98. package/src/lib/app-utils/media-metadata.js +74 -0
  99. package/src/lib/app-utils/media-metadata.js.map +1 -1
  100. package/src/lib/app-utils/message-search.d.ts +40 -0
  101. package/src/lib/app-utils/message-search.js +168 -0
  102. package/src/lib/app-utils/message-search.js.map +1 -0
  103. package/src/lib/app-utils/name-search.d.ts +95 -0
  104. package/src/lib/app-utils/name-search.js +108 -0
  105. package/src/lib/app-utils/name-search.js.map +1 -1
  106. package/src/lib/app-utils/server.d.ts +2 -0
  107. package/src/lib/app-utils/server.js +2 -0
  108. package/src/lib/app-utils/server.js.map +1 -1
  109. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  110. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  111. package/src/server.d.ts +1 -0
  112. package/src/server.js +7 -0
  113. package/src/server.js.map +1 -1
@@ -177,10 +177,11 @@ export declare function datasetReferencedIds(model: DatasetModel, values: Record
177
177
  export declare function datasetIntegrityFields(model: DatasetModel, values: Record<string, unknown> | undefined): {
178
178
  referencedIds?: string[];
179
179
  filterKeys?: string[];
180
+ filterValues?: DatasetFilterValues;
180
181
  };
181
182
  /**
182
- * `datasetIntegrityFields` for a MERGING write (`update`, `set` with `merge`
183
- * or `mergeFields`), where leaving the field out preserves what is stored
183
+ * `datasetIntegrityFields` for a MERGING write (`update`, `set` with
184
+ * `mergeFields`), where leaving the field out preserves what is stored
184
185
  * rather than clearing it.
185
186
  *
186
187
  * A record whose last reference is cleared has to have the field REMOVED, or
@@ -189,19 +190,28 @@ export declare function datasetIntegrityFields(model: DatasetModel, values: Reco
189
190
  * caller's delete sentinel: `deleteField()` in the web SDK,
190
191
  * `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported
191
192
  * because this module is shared by both and must stay SDK-free. The filter
192
- * tokens are cleared the same way, so a record emptied of every filterable
193
+ * fields are cleared the same way, so a record emptied of every filterable
193
194
  * value stops answering the filters its old values did.
195
+ *
196
+ * ⚠️ `filterValues` is a MAP, and it is written WHOLE: `update` and
197
+ * `mergeFields` replace a field outright, so a value cleared since the last
198
+ * write leaves the map with it. `set(…, { merge: true })` does NOT — it merges
199
+ * maps key by key and would keep the cleared value answering its old
200
+ * equality — so a record write never uses it; list the fields in
201
+ * `mergeFields` instead.
194
202
  */
195
203
  export declare function datasetIntegrityUpdate<TClear>(model: DatasetModel, values: Record<string, unknown> | undefined, clear: TClear): {
196
204
  referencedIds: string[] | TClear;
197
205
  filterKeys: string[] | TClear;
206
+ filterValues: DatasetFilterValues | TClear;
198
207
  };
199
208
  /**
200
- * A dataset record's FILTER TOKENS: every question the records table can ask
201
- * of a record, flattened into one array of strings Firestore indexes on its
202
- * own.
209
+ * A dataset record's FILTER TOKENS: every word-level question the records
210
+ * table can ask of a record, flattened into one array of strings Firestore
211
+ * indexes on its own. Its twin, {@link datasetFilterValues}, holds the
212
+ * equalities.
203
213
  *
204
- * ## Why a token array, and not the values
214
+ * ## Why these fields, and not the values
205
215
  *
206
216
  * A record's fields live under `values`, a user-defined, unbounded map that
207
217
  * the index configuration exempts from indexing — auto-indexing it can exceed
@@ -210,10 +220,13 @@ export declare function datasetIntegrityUpdate<TClear>(model: DatasetModel, valu
210
220
  * dataset, runs into the project's composite-index cap, and is deleted by the
211
221
  * next `firestore:indexes` deploy of a file that does not list it.
212
222
  *
213
- * `filterKeys` needs none of that. Firestore's AUTOMATIC single-field index
214
- * serves `where('filterKeys', 'array-contains', token)` with
215
- * `orderBy(documentId())`, so one clause, or one search word, reaches every
216
- * record of any dataset on any project, with no index deploy.
223
+ * `filterKeys` and `filterValues` need none of that. Firestore's AUTOMATIC
224
+ * single-field indexes serve `where('filterKeys', 'array-contains', token)`
225
+ * and `where('filterValues.<field>', '==', value)`, and every automatic index
226
+ * ends in the document name — so any number of those equalities and ONE
227
+ * `array-contains`, under `orderBy(documentId())`, compose by index merging,
228
+ * with no composite index and no index deploy, on any dataset of any project
229
+ * (AGL-3321).
217
230
  *
218
231
  * ## The token grammar
219
232
  *
@@ -255,30 +268,68 @@ export interface DatasetFilterClause {
255
268
  * digit, lower-cased, at most {@link DATASET_FILTER_WORDS_MAX} of them.
256
269
  */
257
270
  export declare function datasetFilterWords(text: string): string[];
271
+ /**
272
+ * Plain text as its stored key — its `=` token's value and its
273
+ * `filterValues` entry: lower-cased, trimmed, clipped to
274
+ * {@link DATASET_FILTER_VALUE_MAX} characters. What the records table asks a
275
+ * typed value to equal.
276
+ */
277
+ export declare function datasetFilterTextKey(value: string): string;
258
278
  /** The tokens described in {@link DATASET_FILTER_KEYS_MAX}'s comment. */
259
279
  export declare function datasetFilterKeys(model: DatasetModel, values: Record<string, unknown> | undefined): string[];
280
+ /** A record's `filterValues`: field id → the value an equality asks for. */
281
+ export type DatasetFilterValues = Record<string, string | number | boolean>;
260
282
  /**
261
- * The one token that serves a filter clause over every record, or null when
262
- * no token can: the clause is then matched over a window of records instead.
283
+ * The query path of a field's `filterValues` entry, or null for a field id
284
+ * no path can name — such a field is stamped no entry, and the records table
285
+ * offers it no equality (its `contains`, served by `filterKeys`, stands).
286
+ */
287
+ export declare function datasetFilterValuePath(fieldId: string): string | null;
288
+ /**
289
+ * A dataset record's FILTER VALUES: `{ [fieldId]: value }`, one scalar per
290
+ * field an equality can be asked of, so the records table puts every
291
+ * equality on its query (AGL-3321).
292
+ *
293
+ * enum text (`validation.options`) the value exactly, as its `=` token;
294
+ * plain text {@link datasetFilterTextKey};
295
+ * bool a boolean;
296
+ * int32 / int64 / float a number.
297
+ *
298
+ * A form or an automation stores every value as text, so `'19.50'` is the
299
+ * number 19.5 and `'true'` the boolean, as in {@link datasetFilterKeys}.
300
+ * Fields with no value, and timestamps, lists, references, maps, bytes,
301
+ * coordinates and nil fields, carry no entry: a list is asked by member
302
+ * (`f:<id>=<member>` in `filterKeys`), and the rest are not asked about by
303
+ * value.
304
+ *
305
+ * ## What it costs
306
+ *
307
+ * Unlike `values`, this map is NOT exempt from indexing — that is its whole
308
+ * point. Each entry is a scalar and costs two automatic index entries
309
+ * (ascending, descending), so a record costs at most two per model field —
310
+ * bounded by the model's field count (the `/v1` API caps a model at 100
311
+ * fields), far inside Firestore's 40,000 entries per document, where
312
+ * `values` was unbounded because a `map` or `sorted` value may hold anything.
313
+ */
314
+ export declare function datasetFilterValues(model: DatasetModel, values: Record<string, unknown> | undefined): DatasetFilterValues;
315
+ /**
316
+ * The `filterKeys` token a word-level clause asks for, or null when it is
317
+ * not one:
263
318
  *
264
- * - enum, boolean, number and list `equals` → the `=` token of the value;
265
- * - plain text `equals` → the lower-cased `=` token;
266
319
  * - plain text `contains` → the `^` token of the value's FIRST word, so a
267
- * served `contains` is a word-prefix match, not a mid-string one;
320
+ * `contains` is a word-prefix match, not a mid-string one, and one word
321
+ * of several is asked (the records table says so);
322
+ * - list `contains` (and `equals`) → the member's `=` token;
323
+ * - enum, boolean, number and plain text `equals` → the `=` token of the
324
+ * value. The records table asks these of `filterValues` instead, which
325
+ * composes with a `contains`; the token remains what the quick search and
326
+ * older readers match.
268
327
  * - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.
269
328
  *
270
329
  * `clause.field` is the field id. The operator names are the grid's: a
271
330
  * select's `equals`, a number's `=`, a boolean's `is`, a list's `contains`.
272
331
  */
273
332
  export declare function datasetFilterToken(model: DatasetModel, clause: DatasetFilterClause): string | null;
274
- /**
275
- * Every token a record must hold to answer a clause, or null when the clause
276
- * is not token-served. The same as {@link datasetFilterToken} except for a
277
- * plain text `contains` of several words, which needs each word's prefix —
278
- * the query can serve only one of them, and the rest are matched over the
279
- * rows it returns, by these same tokens, so both answers agree.
280
- */
281
- export declare function datasetFilterTokens(model: DatasetModel, clause: DatasetFilterClause): string[] | null;
282
333
  /**
283
334
  * The quick-search token for one typed word, or null for a word with no
284
335
  * letters or digits. A word with punctuation inside it is asked by its first
@@ -385,20 +385,23 @@ const isFiniteNumber = (value)=>typeof value === 'number' && Number.isFinite(val
385
385
  * where clearing the last reference has to remove the field.
386
386
  */ export function datasetIntegrityFields(model, values) {
387
387
  const referencedIds = datasetReferencedIds(model, values);
388
- // The records table's filter tokens (`datasetFilterKeys`) travel on the
389
- // same writes for the same reason: a record written without them is
390
- // invisible to every filter the query serves. Omitted when empty, like
391
- // `referencedIds`.
388
+ // The records table's filter fields (`datasetFilterKeys`,
389
+ // `datasetFilterValues`) travel on the same writes for the same reason: a
390
+ // record written without them is invisible to every filter the query
391
+ // serves. Omitted when empty, like `referencedIds`.
392
392
  const filterKeys = datasetFilterKeys(model, values);
393
+ const filterValues = datasetFilterValues(model, values);
393
394
  return _extends({}, referencedIds.length ? {
394
395
  referencedIds
395
396
  } : {}, filterKeys.length ? {
396
397
  filterKeys
398
+ } : {}, Object.keys(filterValues).length ? {
399
+ filterValues
397
400
  } : {});
398
401
  }
399
402
  /**
400
- * `datasetIntegrityFields` for a MERGING write (`update`, `set` with `merge`
401
- * or `mergeFields`), where leaving the field out preserves what is stored
403
+ * `datasetIntegrityFields` for a MERGING write (`update`, `set` with
404
+ * `mergeFields`), where leaving the field out preserves what is stored
402
405
  * rather than clearing it.
403
406
  *
404
407
  * A record whose last reference is cleared has to have the field REMOVED, or
@@ -407,22 +410,32 @@ const isFiniteNumber = (value)=>typeof value === 'number' && Number.isFinite(val
407
410
  * caller's delete sentinel: `deleteField()` in the web SDK,
408
411
  * `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported
409
412
  * because this module is shared by both and must stay SDK-free. The filter
410
- * tokens are cleared the same way, so a record emptied of every filterable
413
+ * fields are cleared the same way, so a record emptied of every filterable
411
414
  * value stops answering the filters its old values did.
415
+ *
416
+ * ⚠️ `filterValues` is a MAP, and it is written WHOLE: `update` and
417
+ * `mergeFields` replace a field outright, so a value cleared since the last
418
+ * write leaves the map with it. `set(…, { merge: true })` does NOT — it merges
419
+ * maps key by key and would keep the cleared value answering its old
420
+ * equality — so a record write never uses it; list the fields in
421
+ * `mergeFields` instead.
412
422
  */ export function datasetIntegrityUpdate(model, values, clear) {
413
423
  const referencedIds = datasetReferencedIds(model, values);
414
424
  const filterKeys = datasetFilterKeys(model, values);
425
+ const filterValues = datasetFilterValues(model, values);
415
426
  return {
416
427
  referencedIds: referencedIds.length ? referencedIds : clear,
417
- filterKeys: filterKeys.length ? filterKeys : clear
428
+ filterKeys: filterKeys.length ? filterKeys : clear,
429
+ filterValues: Object.keys(filterValues).length ? filterValues : clear
418
430
  };
419
431
  }
420
432
  /**
421
- * A dataset record's FILTER TOKENS: every question the records table can ask
422
- * of a record, flattened into one array of strings Firestore indexes on its
423
- * own.
433
+ * A dataset record's FILTER TOKENS: every word-level question the records
434
+ * table can ask of a record, flattened into one array of strings Firestore
435
+ * indexes on its own. Its twin, {@link datasetFilterValues}, holds the
436
+ * equalities.
424
437
  *
425
- * ## Why a token array, and not the values
438
+ * ## Why these fields, and not the values
426
439
  *
427
440
  * A record's fields live under `values`, a user-defined, unbounded map that
428
441
  * the index configuration exempts from indexing — auto-indexing it can exceed
@@ -431,10 +444,13 @@ const isFiniteNumber = (value)=>typeof value === 'number' && Number.isFinite(val
431
444
  * dataset, runs into the project's composite-index cap, and is deleted by the
432
445
  * next `firestore:indexes` deploy of a file that does not list it.
433
446
  *
434
- * `filterKeys` needs none of that. Firestore's AUTOMATIC single-field index
435
- * serves `where('filterKeys', 'array-contains', token)` with
436
- * `orderBy(documentId())`, so one clause, or one search word, reaches every
437
- * record of any dataset on any project, with no index deploy.
447
+ * `filterKeys` and `filterValues` need none of that. Firestore's AUTOMATIC
448
+ * single-field indexes serve `where('filterKeys', 'array-contains', token)`
449
+ * and `where('filterValues.<field>', '==', value)`, and every automatic index
450
+ * ends in the document name — so any number of those equalities and ONE
451
+ * `array-contains`, under `orderBy(documentId())`, compose by index merging,
452
+ * with no composite index and no index deploy, on any dataset of any project
453
+ * (AGL-3321).
438
454
  *
439
455
  * ## The token grammar
440
456
  *
@@ -488,9 +504,15 @@ const isNumeric = (field)=>field.type === 'int32' || field.type === 'int64' || f
488
504
  if (typeof value === 'number' && Number.isFinite(value)) return String(value);
489
505
  return null;
490
506
  }
491
- /** Plain text as its `=` token's value: lower-cased, trimmed, clipped. */ function textKey(value) {
507
+ /**
508
+ * Plain text as its stored key — its `=` token's value and its
509
+ * `filterValues` entry: lower-cased, trimmed, clipped to
510
+ * {@link DATASET_FILTER_VALUE_MAX} characters. What the records table asks a
511
+ * typed value to equal.
512
+ */ export function datasetFilterTextKey(value) {
492
513
  return Array.from(value.trim().toLowerCase()).slice(0, DATASET_FILTER_VALUE_MAX).join('');
493
514
  }
515
+ const textKey = datasetFilterTextKey;
494
516
  /** A stored number (or its text form) as its `=` token's value. */ function numberKey(value) {
495
517
  const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
496
518
  return Number.isFinite(number) ? String(number) : null;
@@ -565,13 +587,76 @@ const isNumeric = (field)=>field.type === 'int32' || field.type === 'int64' || f
565
587
  return kept.sort();
566
588
  }
567
589
  /**
568
- * The one token that serves a filter clause over every record, or null when
569
- * no token can: the clause is then matched over a window of records instead.
590
+ * Field ids a query can name as `filterValues.<id>`. A path string splits on
591
+ * `.`, and the SDKs refuse `~ * / [ ]` in one; the model editor allows only
592
+ * letters, digits and underscores, but a v1 dataset's column names were
593
+ * never checked. Backticks are refused too, as the escape a path would need.
594
+ */ const FILTER_VALUE_ID = /^[^.~*/[\]`]+$/;
595
+ /**
596
+ * The query path of a field's `filterValues` entry, or null for a field id
597
+ * no path can name — such a field is stamped no entry, and the records table
598
+ * offers it no equality (its `contains`, served by `filterKeys`, stands).
599
+ */ export function datasetFilterValuePath(fieldId) {
600
+ return FILTER_VALUE_ID.test(fieldId) && !/^__.*__$/.test(fieldId) ? `filterValues.${fieldId}` : null;
601
+ }
602
+ /**
603
+ * A dataset record's FILTER VALUES: `{ [fieldId]: value }`, one scalar per
604
+ * field an equality can be asked of, so the records table puts every
605
+ * equality on its query (AGL-3321).
606
+ *
607
+ * enum text (`validation.options`) the value exactly, as its `=` token;
608
+ * plain text {@link datasetFilterTextKey};
609
+ * bool a boolean;
610
+ * int32 / int64 / float a number.
611
+ *
612
+ * A form or an automation stores every value as text, so `'19.50'` is the
613
+ * number 19.5 and `'true'` the boolean, as in {@link datasetFilterKeys}.
614
+ * Fields with no value, and timestamps, lists, references, maps, bytes,
615
+ * coordinates and nil fields, carry no entry: a list is asked by member
616
+ * (`f:<id>=<member>` in `filterKeys`), and the rest are not asked about by
617
+ * value.
618
+ *
619
+ * ## What it costs
620
+ *
621
+ * Unlike `values`, this map is NOT exempt from indexing — that is its whole
622
+ * point. Each entry is a scalar and costs two automatic index entries
623
+ * (ascending, descending), so a record costs at most two per model field —
624
+ * bounded by the model's field count (the `/v1` API caps a model at 100
625
+ * fields), far inside Firestore's 40,000 entries per document, where
626
+ * `values` was unbounded because a `map` or `sorted` value may hold anything.
627
+ */ export function datasetFilterValues(model, values) {
628
+ const out = {};
629
+ for (const fieldId of modelFieldIds(model)){
630
+ var _model_fields;
631
+ const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
632
+ const stored = values == null ? void 0 : values[fieldId];
633
+ if (!field || stored == null || !datasetFilterValuePath(fieldId)) continue;
634
+ if (field.type === 'text') {
635
+ const text = asText(stored);
636
+ if (text == null || !text.trim()) continue;
637
+ out[fieldId] = isEnum(field) ? text : textKey(text);
638
+ } else if (field.type === 'bool') {
639
+ const key = boolKey(stored);
640
+ if (key) out[fieldId] = key === 'true';
641
+ } else if (isNumeric(field)) {
642
+ const key = numberKey(stored);
643
+ if (key) out[fieldId] = Number(key);
644
+ }
645
+ }
646
+ return out;
647
+ }
648
+ /**
649
+ * The `filterKeys` token a word-level clause asks for, or null when it is
650
+ * not one:
570
651
  *
571
- * - enum, boolean, number and list `equals` → the `=` token of the value;
572
- * - plain text `equals` → the lower-cased `=` token;
573
652
  * - plain text `contains` → the `^` token of the value's FIRST word, so a
574
- * served `contains` is a word-prefix match, not a mid-string one;
653
+ * `contains` is a word-prefix match, not a mid-string one, and one word
654
+ * of several is asked (the records table says so);
655
+ * - list `contains` (and `equals`) → the member's `=` token;
656
+ * - enum, boolean, number and plain text `equals` → the `=` token of the
657
+ * value. The records table asks these of `filterValues` instead, which
658
+ * composes with a `contains`; the token remains what the quick search and
659
+ * older readers match.
575
660
  * - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.
576
661
  *
577
662
  * `clause.field` is the field id. The operator names are the grid's: a
@@ -607,24 +692,6 @@ const isNumeric = (field)=>field.type === 'int32' || field.type === 'int64' || f
607
692
  }
608
693
  return null;
609
694
  }
610
- /**
611
- * Every token a record must hold to answer a clause, or null when the clause
612
- * is not token-served. The same as {@link datasetFilterToken} except for a
613
- * plain text `contains` of several words, which needs each word's prefix —
614
- * the query can serve only one of them, and the rest are matched over the
615
- * rows it returns, by these same tokens, so both answers agree.
616
- */ export function datasetFilterTokens(model, clause) {
617
- var _model_fields;
618
- const first = datasetFilterToken(model, clause);
619
- if (!first) return null;
620
- const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[clause.field];
621
- if ((field == null ? void 0 : field.type) === 'text' && !isEnum(field) && clause.op === 'contains') {
622
- return datasetFilterWords(clause.value).map((word)=>`f:${clause.field}^${clipWord(word)}`);
623
- }
624
- return [
625
- first
626
- ];
627
- }
628
695
  /**
629
696
  * The quick-search token for one typed word, or null for a word with no
630
697
  * letters or digits. A word with punctuation inside it is asked by its first
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/dataset-models.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { validateCustomFieldValue } from '../plugin-manager/custom-fields'\nimport { type DatasetFieldEntry, humanizeDatasetFieldId } from './datasets'\n\n/**\n * Dataset models (AGL-177): the runtime promotion of the type-level\n * blueprint in `libs/shared/data/types/src/lib/dod.ts` (kept as the\n * referenced design doc — this module is the single runtime source of\n * truth). A model lives on the dataset doc\n * (`orgs/{orgId}/datasets/{id}.model`) and drives the typed editor, import\n * and site-restore validation, and the console and `/v1` record writes\n * through the shared `validateDocument`/`coerceDocumentValues` pair so they\n * can't disagree. Form submissions and automation steps write through\n * `buildDatasetRecordValues` instead, which stores each value as text and\n * validates nothing.\n *\n * Storage conventions (Firestore-safe): timestamps as epoch millis\n * numbers, coordinates as `{ latitude, longitude }`, `sorted` as arrays,\n * `map` as plain objects, references as target-document id strings.\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/** 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 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 * Validates a document's values against a model, returning fieldId →\n * human-readable error (empty object = valid). Values are expected in\n * storage form (see the module doc); use `coerceDocumentValues` first\n * when starting from user input strings.\n */\nexport function validateDocument(\n model: DatasetModel,\n values: Record<string, unknown>,\n): Record<string, string> {\n const errors: Record<string, string> = {}\n for (const fieldId of model.order) {\n const field = model.fields[fieldId]\n if (!field) continue\n const value = values[fieldId]\n const required = field.required || field.validation?.required\n if (value == null || value === '') {\n if (required) errors[fieldId] = `${field.name} is required`\n continue\n }\n const rules = field.validation ?? {}\n switch (field.type) {\n case 'text': {\n if (typeof value !== 'string') {\n errors[fieldId] = `${field.name} must be text`\n break\n }\n if (rules.min != null && value.length < rules.min) {\n errors[fieldId] = `${field.name} must be at least ${rules.min} characters`\n } else if (rules.max != null && value.length > rules.max) {\n errors[fieldId] = `${field.name} must be at most ${rules.max} characters`\n } else if (rules.regex) {\n try {\n if (!new RegExp(rules.regex).test(value)) {\n errors[fieldId] = `${field.name} has an invalid format`\n }\n } catch {\n // Broken pattern in the model never blocks writes.\n }\n }\n if (\n !errors[fieldId] &&\n rules.options?.length &&\n !rules.options.includes(value)\n ) {\n errors[fieldId] = `${field.name} must be one of: ${rules.options.join(', ')}`\n }\n break\n }\n case 'bool':\n if (typeof value !== 'boolean') {\n errors[fieldId] = `${field.name} must be true or false`\n }\n break\n case 'int32':\n case 'int64':\n if (!isFiniteNumber(value) || !Number.isInteger(value)) {\n errors[fieldId] = `${field.name} must be a whole number`\n break\n }\n if (rules.min != null && value < rules.min) {\n errors[fieldId] = `${field.name} must be ≥ ${rules.min}`\n } else if (rules.max != null && value > rules.max) {\n errors[fieldId] = `${field.name} must be ≤ ${rules.max}`\n }\n break\n case 'float':\n case 'timestamp':\n if (!isFiniteNumber(value)) {\n errors[fieldId] =\n field.type === 'timestamp'\n ? `${field.name} must be a date`\n : `${field.name} must be a number`\n break\n }\n if (rules.min != null && value < rules.min) {\n errors[fieldId] = `${field.name} must be ≥ ${rules.min}`\n } else if (rules.max != null && value > rules.max) {\n errors[fieldId] = `${field.name} must be ≤ ${rules.max}`\n }\n break\n case 'coordinates': {\n const coordinates = value as { latitude?: unknown; longitude?: unknown }\n if (\n !isFiniteNumber(coordinates?.latitude) ||\n !isFiniteNumber(coordinates?.longitude) ||\n Math.abs(coordinates.latitude) > 90 ||\n Math.abs(coordinates.longitude) > 180\n ) {\n errors[fieldId] = `${field.name} must be valid coordinates`\n }\n break\n }\n case 'sorted':\n if (!Array.isArray(value)) {\n errors[fieldId] = `${field.name} must be a list`\n }\n break\n case 'map':\n if (typeof value !== 'object' || Array.isArray(value)) {\n errors[fieldId] = `${field.name} must be a map`\n }\n break\n case 'reference':\n if (field.reference?.multiple) {\n if (\n !Array.isArray(value) ||\n value.some((entry) => typeof entry !== 'string' || !entry)\n ) {\n errors[fieldId] = `${field.name} must reference documents`\n }\n } else if (typeof value !== 'string' || !value) {\n errors[fieldId] = `${field.name} must reference a document`\n }\n break\n case 'bytes':\n case 'nil':\n break\n }\n }\n // Custom field types (AGL-434): plugin validators run after the base\n // checks, only on fields that passed them.\n for (const fieldId of model.order) {\n const field = model.fields[fieldId]\n if (!field?.customType || errors[fieldId]) continue\n const customError = validateCustomFieldValue(\n field.customType,\n values[fieldId],\n )\n if (customError) errors[fieldId] = `${field.name}: ${customError}`\n }\n return errors\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 * Coerces user-input strings (form fields, CSV cells) into storage form\n * per the field type. Unparseable input is passed through untouched so\n * `validateDocument` reports it instead of silently mangling it.\n */\nexport function coerceDocumentValues(\n model: DatasetModel,\n input: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n const field = model.fields[fieldId]\n const raw = input[fieldId]\n if (!field || raw == null || raw === '') continue\n if (typeof raw !== 'string') {\n values[fieldId] = raw\n continue\n }\n switch (field.type) {\n case 'bool':\n values[fieldId] =\n raw === 'true' ? true : raw === 'false' ? false : raw\n break\n case 'int32':\n case 'int64': {\n const parsed = Number(raw)\n values[fieldId] = Number.isInteger(parsed) ? parsed : raw\n break\n }\n case 'float': {\n const parsed = Number(raw)\n values[fieldId] = Number.isFinite(parsed) ? parsed : raw\n break\n }\n case 'timestamp': {\n const millis = Date.parse(raw)\n values[fieldId] = Number.isFinite(millis) ? millis : raw\n break\n }\n case 'coordinates': {\n const [latitude, longitude] = raw.split(',').map((part) => Number(part.trim()))\n values[fieldId] =\n Number.isFinite(latitude) && Number.isFinite(longitude)\n ? { latitude, longitude }\n : raw\n break\n }\n case 'sorted':\n values[fieldId] = raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n break\n case 'map':\n try {\n values[fieldId] = JSON.parse(raw)\n } catch {\n values[fieldId] = raw\n }\n break\n case 'reference':\n values[fieldId] = field.reference?.multiple\n ? raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n : raw\n break\n default:\n values[fieldId] = raw\n }\n }\n return values\n}\n\n/**\n * Every reference id a record holds, flattened out of its `reference` fields\n * into one de-duplicated list.\n *\n * A reference field stores either a single target id or — when\n * `reference.multiple` is set — an array of them, and a model may declare\n * several. Both shapes flatten into the same list because the question asked\n * of it is never \"which field\", it is \"does this record still point at that\n * document\".\n *\n * Fields are read from `order` UNIONED with the keys of `fields`. Every other\n * pass over a model walks `order` alone, which is right for anything the user\n * sees — but a reference declared without a display slot still holds a real\n * FKey, and an integrity index that skipped it would under-report.\n */\nexport function datasetReferencedIds(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n): string[] {\n const ids = new Set<string>()\n const fieldIds = new Set([\n ...(model.order ?? []),\n ...Object.keys(model.fields ?? {}),\n ])\n for (const fieldId of fieldIds) {\n if (model.fields?.[fieldId]?.type !== 'reference') continue\n const stored = values?.[fieldId]\n for (const entry of Array.isArray(stored) ? stored : [stored]) {\n const id = typeof entry === 'string' ? entry.trim() : ''\n if (id) ids.add(id)\n }\n }\n return [...ids]\n}\n\n/**\n * The integrity index a record write carries, so referential integrity can be\n * decided by a QUERY rather than by the rows a listener happened to fetch.\n *\n * Deleting a record has to answer \"is anything still pointing at this?\", and\n * `restrict` refuses the delete on a yes while `setNull` strips the FKey. That\n * question cannot be asked where the answer is stored: dataset fields are\n * user-defined, so `records.values` carries a deliberate index exemption in\n * `cloud/firebase-firestore.indexes.json` — auto-indexing an unbounded map\n * blows Firestore's per-document index-entry limit — and\n * `where('values.<fieldId>', '==', id)` is therefore FAILED_PRECONDITION.\n * Reading a page of records and filtering them in the browser answers only for\n * the rows in that page, which is a browse window standing in for a\n * correctness check: a reference held outside it reads as no reference at all,\n * the delete proceeds, and `restrict` silently fails to restrict.\n *\n * `referencedIds` is that same reference, denormalized to a top-level array\n * Firestore will index, so one `array-contains` reaches every record.\n *\n * ⚠️ OMITTED when a record references nothing, rather than written as `[]`.\n * `isNotEmpty` is served as `!= null` and an empty array is not null, so a\n * record stamped `[]` answers \"holds a reference\" for a dataset that holds\n * none.\n *\n * ⚠️ Spread this at EVERY write that sets a record's `values`. A write that\n * sets values without it leaves the index describing the PREVIOUS values, and\n * a stale integrity index is the exact failure it exists to prevent — with the\n * UI reporting the record as safe to delete. On a merging write use\n * `datasetIntegrityUpdate` instead: omission leaves a stale array in place\n * where clearing the last reference has to remove the field.\n */\nexport function datasetIntegrityFields(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n): { referencedIds?: string[]; filterKeys?: string[] } {\n const referencedIds = datasetReferencedIds(model, values)\n // The records table's filter tokens (`datasetFilterKeys`) travel on the\n // same writes for the same reason: a record written without them is\n // invisible to every filter the query serves. Omitted when empty, like\n // `referencedIds`.\n const filterKeys = datasetFilterKeys(model, values)\n return {\n ...(referencedIds.length ? { referencedIds } : {}),\n ...(filterKeys.length ? { filterKeys } : {}),\n }\n}\n\n/**\n * `datasetIntegrityFields` for a MERGING write (`update`, `set` with `merge`\n * or `mergeFields`), where leaving the field out preserves what is stored\n * rather than clearing it.\n *\n * A record whose last reference is cleared has to have the field REMOVED, or\n * it goes on answering the `array-contains` for a document it no longer points\n * at — a `restrict` that refuses a delete nothing is holding. `clear` is the\n * caller's delete sentinel: `deleteField()` in the web SDK,\n * `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported\n * because this module is shared by both and must stay SDK-free. The filter\n * tokens are cleared the same way, so a record emptied of every filterable\n * value stops answering the filters its old values did.\n */\nexport function datasetIntegrityUpdate<TClear>(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n clear: TClear,\n): { referencedIds: string[] | TClear; filterKeys: string[] | TClear } {\n const referencedIds = datasetReferencedIds(model, values)\n const filterKeys = datasetFilterKeys(model, values)\n return {\n referencedIds: referencedIds.length ? referencedIds : clear,\n filterKeys: filterKeys.length ? filterKeys : clear,\n }\n}\n\n/**\n * A dataset record's FILTER TOKENS: every question the records table can ask\n * of a record, flattened into one array of strings Firestore indexes on its\n * own.\n *\n * ## Why a token array, 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` needs none of that. Firestore's AUTOMATIC single-field index\n * serves `where('filterKeys', 'array-contains', token)` with\n * `orderBy(documentId())`, so one clause, or one search word, reaches every\n * record of any dataset on any project, with no index deploy.\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/** Plain text as its `=` token's value: lower-cased, trimmed, clipped. */\nfunction textKey(value: string): string {\n return Array.from(value.trim().toLowerCase())\n .slice(0, DATASET_FILTER_VALUE_MAX)\n .join('')\n}\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/**\n * The one token that serves a filter clause over every record, or null when\n * no token can: the clause is then matched over a window of records instead.\n *\n * - enum, boolean, number and list `equals` → the `=` token of the value;\n * - plain text `equals` → the lower-cased `=` token;\n * - plain text `contains` → the `^` token of the value's FIRST word, so a\n * served `contains` is a word-prefix match, not a mid-string one;\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 * Every token a record must hold to answer a clause, or null when the clause\n * is not token-served. The same as {@link datasetFilterToken} except for a\n * plain text `contains` of several words, which needs each word's prefix —\n * the query can serve only one of them, and the rest are matched over the\n * rows it returns, by these same tokens, so both answers agree.\n */\nexport function datasetFilterTokens(\n model: DatasetModel,\n clause: DatasetFilterClause,\n): string[] | null {\n const first = datasetFilterToken(model, clause)\n if (!first) return null\n const field = model.fields?.[clause.field]\n if (field?.type === 'text' && !isEnum(field) && clause.op === 'contains') {\n return datasetFilterWords(clause.value).map(\n (word) => `f:${clause.field}^${clipWord(word)}`,\n )\n }\n return [first]\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"],"names":["validateCustomFieldValue","humanizeDatasetFieldId","DATASET_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","id","effectiveDatasetModel","dataset","length","isFiniteNumber","value","Number","isFinite","validateDocument","values","errors","fieldId","field","required","validation","rules","min","max","regex","RegExp","test","options","includes","join","isInteger","latitude","longitude","Math","abs","Array","isArray","multiple","some","customType","customError","formatDatasetValue","String","Date","toISOString","slice","replace","JSON","stringify","datasetValueToInput","coerceDocumentValues","input","raw","parsed","millis","parse","split","part","trim","filter","Boolean","datasetReferencedIds","ids","Set","fieldIds","Object","keys","stored","add","datasetIntegrityFields","referencedIds","filterKeys","datasetFilterKeys","datasetIntegrityUpdate","clear","DATASET_FILTER_KEYS_MAX","DATASET_FILTER_VALUE_MAX","DATASET_FILTER_PREFIX_MAX","DATASET_FILTER_WORDS_MAX","datasetFilterWords","toLowerCase","prefixes","word","chars","from","_char","at","clipWord","isEnum","isNumeric","asText","textKey","numberKey","number","NaN","boolKey","modelFieldIds","exact","fieldWords","search","searchable","prefix","key","member","kept","sort","datasetFilterToken","clause","op","equals","first","datasetFilterTokens","datasetSearchToken"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/dataset-models.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { validateCustomFieldValue } from '../plugin-manager/custom-fields'\nimport { type DatasetFieldEntry, humanizeDatasetFieldId } from './datasets'\n\n/**\n * Dataset models (AGL-177): the runtime promotion of the type-level\n * blueprint in `libs/shared/data/types/src/lib/dod.ts` (kept as the\n * referenced design doc — this module is the single runtime source of\n * truth). A model lives on the dataset doc\n * (`orgs/{orgId}/datasets/{id}.model`) and drives the typed editor, import\n * and site-restore validation, and the console and `/v1` record writes\n * through the shared `validateDocument`/`coerceDocumentValues` pair so they\n * can't disagree. Form submissions and automation steps write through\n * `buildDatasetRecordValues` instead, which stores each value as text and\n * validates nothing.\n *\n * Storage conventions (Firestore-safe): timestamps as epoch millis\n * numbers, coordinates as `{ latitude, longitude }`, `sorted` as arrays,\n * `map` as plain objects, references as target-document id strings.\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/** 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 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 * Validates a document's values against a model, returning fieldId →\n * human-readable error (empty object = valid). Values are expected in\n * storage form (see the module doc); use `coerceDocumentValues` first\n * when starting from user input strings.\n */\nexport function validateDocument(\n model: DatasetModel,\n values: Record<string, unknown>,\n): Record<string, string> {\n const errors: Record<string, string> = {}\n for (const fieldId of model.order) {\n const field = model.fields[fieldId]\n if (!field) continue\n const value = values[fieldId]\n const required = field.required || field.validation?.required\n if (value == null || value === '') {\n if (required) errors[fieldId] = `${field.name} is required`\n continue\n }\n const rules = field.validation ?? {}\n switch (field.type) {\n case 'text': {\n if (typeof value !== 'string') {\n errors[fieldId] = `${field.name} must be text`\n break\n }\n if (rules.min != null && value.length < rules.min) {\n errors[fieldId] = `${field.name} must be at least ${rules.min} characters`\n } else if (rules.max != null && value.length > rules.max) {\n errors[fieldId] = `${field.name} must be at most ${rules.max} characters`\n } else if (rules.regex) {\n try {\n if (!new RegExp(rules.regex).test(value)) {\n errors[fieldId] = `${field.name} has an invalid format`\n }\n } catch {\n // Broken pattern in the model never blocks writes.\n }\n }\n if (\n !errors[fieldId] &&\n rules.options?.length &&\n !rules.options.includes(value)\n ) {\n errors[fieldId] = `${field.name} must be one of: ${rules.options.join(', ')}`\n }\n break\n }\n case 'bool':\n if (typeof value !== 'boolean') {\n errors[fieldId] = `${field.name} must be true or false`\n }\n break\n case 'int32':\n case 'int64':\n if (!isFiniteNumber(value) || !Number.isInteger(value)) {\n errors[fieldId] = `${field.name} must be a whole number`\n break\n }\n if (rules.min != null && value < rules.min) {\n errors[fieldId] = `${field.name} must be ≥ ${rules.min}`\n } else if (rules.max != null && value > rules.max) {\n errors[fieldId] = `${field.name} must be ≤ ${rules.max}`\n }\n break\n case 'float':\n case 'timestamp':\n if (!isFiniteNumber(value)) {\n errors[fieldId] =\n field.type === 'timestamp'\n ? `${field.name} must be a date`\n : `${field.name} must be a number`\n break\n }\n if (rules.min != null && value < rules.min) {\n errors[fieldId] = `${field.name} must be ≥ ${rules.min}`\n } else if (rules.max != null && value > rules.max) {\n errors[fieldId] = `${field.name} must be ≤ ${rules.max}`\n }\n break\n case 'coordinates': {\n const coordinates = value as { latitude?: unknown; longitude?: unknown }\n if (\n !isFiniteNumber(coordinates?.latitude) ||\n !isFiniteNumber(coordinates?.longitude) ||\n Math.abs(coordinates.latitude) > 90 ||\n Math.abs(coordinates.longitude) > 180\n ) {\n errors[fieldId] = `${field.name} must be valid coordinates`\n }\n break\n }\n case 'sorted':\n if (!Array.isArray(value)) {\n errors[fieldId] = `${field.name} must be a list`\n }\n break\n case 'map':\n if (typeof value !== 'object' || Array.isArray(value)) {\n errors[fieldId] = `${field.name} must be a map`\n }\n break\n case 'reference':\n if (field.reference?.multiple) {\n if (\n !Array.isArray(value) ||\n value.some((entry) => typeof entry !== 'string' || !entry)\n ) {\n errors[fieldId] = `${field.name} must reference documents`\n }\n } else if (typeof value !== 'string' || !value) {\n errors[fieldId] = `${field.name} must reference a document`\n }\n break\n case 'bytes':\n case 'nil':\n break\n }\n }\n // Custom field types (AGL-434): plugin validators run after the base\n // checks, only on fields that passed them.\n for (const fieldId of model.order) {\n const field = model.fields[fieldId]\n if (!field?.customType || errors[fieldId]) continue\n const customError = validateCustomFieldValue(\n field.customType,\n values[fieldId],\n )\n if (customError) errors[fieldId] = `${field.name}: ${customError}`\n }\n return errors\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 * Coerces user-input strings (form fields, CSV cells) into storage form\n * per the field type. Unparseable input is passed through untouched so\n * `validateDocument` reports it instead of silently mangling it.\n */\nexport function coerceDocumentValues(\n model: DatasetModel,\n input: Record<string, unknown>,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const fieldId of model.order) {\n const field = model.fields[fieldId]\n const raw = input[fieldId]\n if (!field || raw == null || raw === '') continue\n if (typeof raw !== 'string') {\n values[fieldId] = raw\n continue\n }\n switch (field.type) {\n case 'bool':\n values[fieldId] =\n raw === 'true' ? true : raw === 'false' ? false : raw\n break\n case 'int32':\n case 'int64': {\n const parsed = Number(raw)\n values[fieldId] = Number.isInteger(parsed) ? parsed : raw\n break\n }\n case 'float': {\n const parsed = Number(raw)\n values[fieldId] = Number.isFinite(parsed) ? parsed : raw\n break\n }\n case 'timestamp': {\n const millis = Date.parse(raw)\n values[fieldId] = Number.isFinite(millis) ? millis : raw\n break\n }\n case 'coordinates': {\n const [latitude, longitude] = raw.split(',').map((part) => Number(part.trim()))\n values[fieldId] =\n Number.isFinite(latitude) && Number.isFinite(longitude)\n ? { latitude, longitude }\n : raw\n break\n }\n case 'sorted':\n values[fieldId] = raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n break\n case 'map':\n try {\n values[fieldId] = JSON.parse(raw)\n } catch {\n values[fieldId] = raw\n }\n break\n case 'reference':\n values[fieldId] = field.reference?.multiple\n ? raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n : raw\n break\n default:\n values[fieldId] = raw\n }\n }\n return values\n}\n\n/**\n * Every reference id a record holds, flattened out of its `reference` fields\n * into one de-duplicated list.\n *\n * A reference field stores either a single target id or — when\n * `reference.multiple` is set — an array of them, and a model may declare\n * several. Both shapes flatten into the same list because the question asked\n * of it is never \"which field\", it is \"does this record still point at that\n * document\".\n *\n * Fields are read from `order` UNIONED with the keys of `fields`. Every other\n * pass over a model walks `order` alone, which is right for anything the user\n * sees — but a reference declared without a display slot still holds a real\n * FKey, and an integrity index that skipped it would under-report.\n */\nexport function datasetReferencedIds(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n): string[] {\n const ids = new Set<string>()\n const fieldIds = new Set([\n ...(model.order ?? []),\n ...Object.keys(model.fields ?? {}),\n ])\n for (const fieldId of fieldIds) {\n if (model.fields?.[fieldId]?.type !== 'reference') continue\n const stored = values?.[fieldId]\n for (const entry of Array.isArray(stored) ? stored : [stored]) {\n const id = typeof entry === 'string' ? entry.trim() : ''\n if (id) ids.add(id)\n }\n }\n return [...ids]\n}\n\n/**\n * The integrity index a record write carries, so referential integrity can be\n * decided by a QUERY rather than by the rows a listener happened to fetch.\n *\n * Deleting a record has to answer \"is anything still pointing at this?\", and\n * `restrict` refuses the delete on a yes while `setNull` strips the FKey. That\n * question cannot be asked where the answer is stored: dataset fields are\n * user-defined, so `records.values` carries a deliberate index exemption in\n * `cloud/firebase-firestore.indexes.json` — auto-indexing an unbounded map\n * blows Firestore's per-document index-entry limit — and\n * `where('values.<fieldId>', '==', id)` is therefore FAILED_PRECONDITION.\n * Reading a page of records and filtering them in the browser answers only for\n * the rows in that page, which is a browse window standing in for a\n * correctness check: a reference held outside it reads as no reference at all,\n * the delete proceeds, and `restrict` silently fails to restrict.\n *\n * `referencedIds` is that same reference, denormalized to a top-level array\n * Firestore will index, so one `array-contains` reaches every record.\n *\n * ⚠️ OMITTED when a record references nothing, rather than written as `[]`.\n * `isNotEmpty` is served as `!= null` and an empty array is not null, so a\n * record stamped `[]` answers \"holds a reference\" for a dataset that holds\n * none.\n *\n * ⚠️ Spread this at EVERY write that sets a record's `values`. A write that\n * sets values without it leaves the index describing the PREVIOUS values, and\n * a stale integrity index is the exact failure it exists to prevent — with the\n * UI reporting the record as safe to delete. On a merging write use\n * `datasetIntegrityUpdate` instead: omission leaves a stale array in place\n * where clearing the last reference has to remove the field.\n */\nexport function datasetIntegrityFields(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n): {\n referencedIds?: string[]\n filterKeys?: string[]\n filterValues?: DatasetFilterValues\n} {\n const referencedIds = datasetReferencedIds(model, values)\n // The records table's filter fields (`datasetFilterKeys`,\n // `datasetFilterValues`) travel on the same writes for the same reason: a\n // record written without them is invisible to every filter the query\n // serves. Omitted when empty, like `referencedIds`.\n const filterKeys = datasetFilterKeys(model, values)\n const filterValues = datasetFilterValues(model, values)\n return {\n ...(referencedIds.length ? { referencedIds } : {}),\n ...(filterKeys.length ? { filterKeys } : {}),\n ...(Object.keys(filterValues).length ? { filterValues } : {}),\n }\n}\n\n/**\n * `datasetIntegrityFields` for a MERGING write (`update`, `set` with\n * `mergeFields`), where leaving the field out preserves what is stored\n * rather than clearing it.\n *\n * A record whose last reference is cleared has to have the field REMOVED, or\n * it goes on answering the `array-contains` for a document it no longer points\n * at — a `restrict` that refuses a delete nothing is holding. `clear` is the\n * caller's delete sentinel: `deleteField()` in the web SDK,\n * `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported\n * because this module is shared by both and must stay SDK-free. The filter\n * fields are cleared the same way, so a record emptied of every filterable\n * value stops answering the filters its old values did.\n *\n * ⚠️ `filterValues` is a MAP, and it is written WHOLE: `update` and\n * `mergeFields` replace a field outright, so a value cleared since the last\n * write leaves the map with it. `set(…, { merge: true })` does NOT — it merges\n * maps key by key and would keep the cleared value answering its old\n * equality — so a record write never uses it; list the fields in\n * `mergeFields` instead.\n */\nexport function datasetIntegrityUpdate<TClear>(\n model: DatasetModel,\n values: Record<string, unknown> | undefined,\n clear: TClear,\n): {\n referencedIds: string[] | TClear\n filterKeys: string[] | TClear\n filterValues: DatasetFilterValues | TClear\n} {\n const referencedIds = datasetReferencedIds(model, values)\n const filterKeys = datasetFilterKeys(model, values)\n const filterValues = datasetFilterValues(model, values)\n return {\n referencedIds: referencedIds.length ? referencedIds : clear,\n filterKeys: filterKeys.length ? filterKeys : clear,\n filterValues: Object.keys(filterValues).length ? filterValues : clear,\n }\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"],"names":["validateCustomFieldValue","humanizeDatasetFieldId","DATASET_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","id","effectiveDatasetModel","dataset","length","isFiniteNumber","value","Number","isFinite","validateDocument","values","errors","fieldId","field","required","validation","rules","min","max","regex","RegExp","test","options","includes","join","isInteger","latitude","longitude","Math","abs","Array","isArray","multiple","some","customType","customError","formatDatasetValue","String","Date","toISOString","slice","replace","JSON","stringify","datasetValueToInput","coerceDocumentValues","input","raw","parsed","millis","parse","split","part","trim","filter","Boolean","datasetReferencedIds","ids","Set","fieldIds","Object","keys","stored","add","datasetIntegrityFields","referencedIds","filterKeys","datasetFilterKeys","filterValues","datasetFilterValues","datasetIntegrityUpdate","clear","DATASET_FILTER_KEYS_MAX","DATASET_FILTER_VALUE_MAX","DATASET_FILTER_PREFIX_MAX","DATASET_FILTER_WORDS_MAX","datasetFilterWords","toLowerCase","prefixes","word","chars","from","_char","at","clipWord","isEnum","isNumeric","asText","datasetFilterTextKey","textKey","numberKey","number","NaN","boolKey","modelFieldIds","exact","fieldWords","search","searchable","prefix","key","member","kept","sort","FILTER_VALUE_ID","datasetFilterValuePath","out","datasetFilterToken","clause","op","equals","first","datasetSearchToken"],"mappings":"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"}