@aglyn/aglyn 1.0.0-beta.194 → 1.0.0-beta.196
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/collection-entry-date.d.ts +42 -0
- package/src/lib/app-utils/collection-entry-date.js +40 -0
- package/src/lib/app-utils/collection-entry-date.js.map +1 -1
- package/src/lib/app-utils/dataset-models.d.ts +95 -1
- package/src/lib/app-utils/dataset-models.js +233 -4
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +30 -9
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +16 -1
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +53 -6
- package/src/lib/app-utils/platform-brand.js +64 -8
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/system-email-chrome.d.ts +130 -0
- package/src/lib/app-utils/system-email-chrome.js +171 -0
- package/src/lib/app-utils/system-email-chrome.js.map +1 -0
- package/src/lib/plugin-manager/plugin-text-generation.d.ts +137 -0
- package/src/lib/plugin-manager/plugin-text-generation.js +43 -0
- package/src/lib/plugin-manager/plugin-text-generation.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.196",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,16 +33,16 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.196",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.196",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.196",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.196",
|
|
40
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.196",
|
|
41
|
+
"@aglyn/shared-util-http": "1.0.0-beta.196",
|
|
42
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.196",
|
|
43
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.196",
|
|
44
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.196",
|
|
45
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.196",
|
|
46
46
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
47
47
|
"@msgpack/msgpack": "^3.1.3",
|
|
48
48
|
"@types/unist": "^3.0.3",
|
|
@@ -180,3 +180,45 @@ timeZoneName?: string): string;
|
|
|
180
180
|
export declare function collectionEntryPublishedAtIso(publishedAt: {
|
|
181
181
|
seconds: number;
|
|
182
182
|
} | null | undefined): string;
|
|
183
|
+
/**
|
|
184
|
+
* The stored field the console's entries table sorts its Published column by
|
|
185
|
+
* (AGL-3323): the instant {@link entryPublishSortStamp} names, kept on the
|
|
186
|
+
* entry beside the two fields it is derived from.
|
|
187
|
+
*
|
|
188
|
+
* It exists because Firestore cannot sort on a fallback. `orderBy` reads one
|
|
189
|
+
* field, and an entry that lacks it is not sorted last, it is left out of the
|
|
190
|
+
* ordered read entirely. A scheduled entry has no `publishedAt` until it goes
|
|
191
|
+
* live, so a table ordered on `publishedAt` could only append it after every
|
|
192
|
+
* published entry — the last page, in both directions — while the cell above
|
|
193
|
+
* it showed its `publishAt`. One stored field holding the date the cell shows
|
|
194
|
+
* is the only shape an index can walk.
|
|
195
|
+
*
|
|
196
|
+
* It is a CONSOLE key. The public site's listings go on sorting and filtering
|
|
197
|
+
* `publishedAt`, which only a live entry carries; this field must never become
|
|
198
|
+
* what decides what a visitor sees, because a scheduled entry carries it too.
|
|
199
|
+
*/
|
|
200
|
+
export declare const ENTRY_PUBLISH_SORT_FIELD = "publishSortAt";
|
|
201
|
+
/**
|
|
202
|
+
* The one instant an entry is listed under in the console (AGL-3206,
|
|
203
|
+
* AGL-3323): its scheduled `publishAt` while it waits to go out, its
|
|
204
|
+
* `publishedAt` otherwise, and `null` when it has neither.
|
|
205
|
+
*
|
|
206
|
+
* Scheduled wins over `publishedAt` because a SCHEDULED entry is not on the
|
|
207
|
+
* site: scheduling a published entry takes it off the live set until the date
|
|
208
|
+
* arrives, and it keeps the old `publishedAt` only until the runtime flips it
|
|
209
|
+
* back and overwrites that with `publishAt`. The date it will go out on is
|
|
210
|
+
* the true one for it.
|
|
211
|
+
*
|
|
212
|
+
* The `status` guard is load-bearing in the other direction too. A draft that
|
|
213
|
+
* still holds a `publishAt` from a cancelled schedule is undated here rather
|
|
214
|
+
* than sorting among the dated rows on a date it never displays.
|
|
215
|
+
*
|
|
216
|
+
* Every writer of `status`, `publishedAt` or `publishAt` stores this value
|
|
217
|
+
* under {@link ENTRY_PUBLISH_SORT_FIELD} — and removes the field on `null` —
|
|
218
|
+
* so the stored key and the rendered cell answer from the same rule.
|
|
219
|
+
*/
|
|
220
|
+
export declare function entryPublishSortStamp<T>(entry: {
|
|
221
|
+
status?: unknown;
|
|
222
|
+
publishedAt?: T | null;
|
|
223
|
+
publishAt?: T | null;
|
|
224
|
+
} | null | undefined): T | null;
|
|
@@ -283,5 +283,45 @@ export function formatCollectionEntryDate(publishedAt, format, locale = COLLECTI
|
|
|
283
283
|
const date = new Date(seconds * 1000);
|
|
284
284
|
return Number.isNaN(date.getTime()) ? '' : date.toISOString();
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The stored field the console's entries table sorts its Published column by
|
|
288
|
+
* (AGL-3323): the instant {@link entryPublishSortStamp} names, kept on the
|
|
289
|
+
* entry beside the two fields it is derived from.
|
|
290
|
+
*
|
|
291
|
+
* It exists because Firestore cannot sort on a fallback. `orderBy` reads one
|
|
292
|
+
* field, and an entry that lacks it is not sorted last, it is left out of the
|
|
293
|
+
* ordered read entirely. A scheduled entry has no `publishedAt` until it goes
|
|
294
|
+
* live, so a table ordered on `publishedAt` could only append it after every
|
|
295
|
+
* published entry — the last page, in both directions — while the cell above
|
|
296
|
+
* it showed its `publishAt`. One stored field holding the date the cell shows
|
|
297
|
+
* is the only shape an index can walk.
|
|
298
|
+
*
|
|
299
|
+
* It is a CONSOLE key. The public site's listings go on sorting and filtering
|
|
300
|
+
* `publishedAt`, which only a live entry carries; this field must never become
|
|
301
|
+
* what decides what a visitor sees, because a scheduled entry carries it too.
|
|
302
|
+
*/ export const ENTRY_PUBLISH_SORT_FIELD = 'publishSortAt';
|
|
303
|
+
/**
|
|
304
|
+
* The one instant an entry is listed under in the console (AGL-3206,
|
|
305
|
+
* AGL-3323): its scheduled `publishAt` while it waits to go out, its
|
|
306
|
+
* `publishedAt` otherwise, and `null` when it has neither.
|
|
307
|
+
*
|
|
308
|
+
* Scheduled wins over `publishedAt` because a SCHEDULED entry is not on the
|
|
309
|
+
* site: scheduling a published entry takes it off the live set until the date
|
|
310
|
+
* arrives, and it keeps the old `publishedAt` only until the runtime flips it
|
|
311
|
+
* back and overwrites that with `publishAt`. The date it will go out on is
|
|
312
|
+
* the true one for it.
|
|
313
|
+
*
|
|
314
|
+
* The `status` guard is load-bearing in the other direction too. A draft that
|
|
315
|
+
* still holds a `publishAt` from a cancelled schedule is undated here rather
|
|
316
|
+
* than sorting among the dated rows on a date it never displays.
|
|
317
|
+
*
|
|
318
|
+
* Every writer of `status`, `publishedAt` or `publishAt` stores this value
|
|
319
|
+
* under {@link ENTRY_PUBLISH_SORT_FIELD} — and removes the field on `null` —
|
|
320
|
+
* so the stored key and the rendered cell answer from the same rule.
|
|
321
|
+
*/ export function entryPublishSortStamp(entry) {
|
|
322
|
+
var _ref;
|
|
323
|
+
if ((entry == null ? void 0 : entry.status) === 'scheduled' && entry.publishAt) return entry.publishAt;
|
|
324
|
+
return (_ref = entry == null ? void 0 : entry.publishedAt) != null ? _ref : null;
|
|
325
|
+
}
|
|
286
326
|
|
|
287
327
|
//# sourceMappingURL=collection-entry-date.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/collection-entry-date.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How an entry's published date is spelled, and the one function that spells\n * it.\n *\n * Its own module because a published page's client bundle reads\n * `formatCollectionEntryDate` and nothing else here: `collection-entries.ts`\n * carries the whole collection runtime — routing, pagination, category links,\n * the search index and the four block expanders — and a bundler cannot drop\n * the rest of it around a single named import.\n */\n\n/**\n * How an entry's published date reads (AGL-1459).\n *\n * Named shapes rather than a format string, for the same reason the styles\n * panel offers a unit picker rather than a CSS box: an editor choosing a\n * byline is not choosing a grammar, and a free-typed pattern is a field where\n * every typo renders as itself on a published page.\n */\nexport type CollectionEntryDateFormat =\n 'default' | 'monthYear' | 'mediumDate' | 'longDate' | 'iso'\n\n/**\n * \"However the site already renders it\" — a REAL value, never absence\n * (AGL-1459).\n *\n * `''` is the shape AGL-1451/AGL-1453 closed repo-wide: an emptied field\n * cannot survive a save, so an author who tried a format and wanted the\n * original back would have no option to pick. So the do-nothing choice is a\n * word, exactly like `COLLECTION_ALL_PILL_NONE` in `collection-entries.ts`.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_DEFAULT: CollectionEntryDateFormat =\n 'default'\n\n/**\n * The offered formats, with the labels an author reads (AGL-1459). Lives here\n * rather than in the block's schema so the list and the formatter cannot\n * drift into offering a shape nothing knows how to produce.\n *\n * Example dates in the labels are written the way the default runtime renders\n * them, which is what makes the choice legible without opening a preview.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_OPTIONS: readonly {\n value: CollectionEntryDateFormat\n label: string\n}[] = [\n { value: 'default', label: 'Site default — 8/9/2026' },\n { value: 'monthYear', label: 'Month and year — Aug 2026' },\n { value: 'mediumDate', label: 'Short date — Aug 9, 2026' },\n { value: 'longDate', label: 'Long date — August 9, 2026' },\n { value: 'iso', label: 'ISO — 2026-08-09' },\n]\n\n/** Any stored value read back as a format this module knows (AGL-1459). */\nexport function normalizeCollectionEntryDateFormat(\n value: unknown,\n): CollectionEntryDateFormat {\n const wanted = String(value ?? '').trim()\n const match = COLLECTION_ENTRY_DATE_FORMAT_OPTIONS.find(\n (option) => option.value === wanted,\n )\n return match?.value ?? COLLECTION_ENTRY_DATE_FORMAT_DEFAULT\n}\n\n/**\n * One entry's published date, in the shape the author asked for (AGL-1459).\n *\n * **The formatting lives HERE, beside the timestamp, and deliberately not in\n * the block.** By the time a date reaches the component it is already a\n * formatted string, and re-parsing one is ambiguous by construction: the same\n * `8/9/2026` reads as 9 August under an `en-US` runtime and 8 September under\n * `en-GB`. A byline that silently moves an article by three weeks is far worse\n * than one that is the wrong shape.\n *\n * `default` returns exactly `toLocaleDateString()` — the string this function\n * replaced, character for character — because the block is live on published\n * entries and opening a dropdown must not restyle them.\n *\n * ## Why the runtime gets no say (AGL-1926)\n *\n * Every branch pins BOTH the locale and the time zone, and the answer is a\n * pure function of the timestamp. It used to pass `locale` straight through\n * (normally `undefined`) with no `timeZone` at all, so the output was a\n * function of the RUNTIME: `en-US` + UTC on a Vercel server, the visitor's\n * own locale and zone in their browser. That is fine while only the server\n * ever calls it — the string is stamped into node props at compose time and\n * both sides then agree on it — but it makes the function a hydration\n * mismatch waiting for its first client-side caller, and `catch-all-client`\n * was exactly that caller. A post published at 02:30 UTC is dated the 10th by\n * the server and the 9th by every visitor west of Greenwich; React reports\n * the difference as a text mismatch (the live React #418 on tenant-web,\n * AGL-1926) and then reconciles against a DOM it no longer describes, which\n * is where the `removeChild`/`insertBefore` pair comes from.\n *\n * The pinned values are the ones production already emits, so no published\n * page changes: Vercel runs UTC with an `en-US` ICU default, which is why\n * every entry date served from aglyn.com today reads `8/9/2026`. Pinning\n * makes that byte-for-byte guaranteed instead of a property of the host.\n *\n * `locale` still exists for a caller that must render in a different one; it\n * now defaults to the value the server was picking implicitly rather than to\n * \"whatever this runtime happens to be\".\n */\nexport const COLLECTION_ENTRY_DATE_LOCALE = 'en-US'\n\n/**\n * The zone a calendar day is read in when a site has not named one.\n *\n * Entry timestamps are absolute instants; the day they are ATTRIBUTED to is\n * an editorial fact about the PUBLISHER, and `timeZone` below is how a site\n * states it. UTC remains the default because it is the answer every existing\n * site is already rendering — a site that never sets a zone must not have its\n * archive move by a day on deploy.\n *\n * ⚑ What AGL-1926 needed was not UTC, it was a zone BOTH SIDES AGREE ON. A\n * per-site zone is still that: it is resolved once, server-side, and travels\n * in the node props, so the client re-render reads the same string. What\n * would reopen the React #418 crash is reading the VISITOR's zone, because\n * only one side of the render knows it.\n */\nexport const COLLECTION_ENTRY_DATE_TIME_ZONE = 'UTC'\n\n/**\n * Is this a zone the runtime can actually format in (AGL-3237)?\n *\n * `Intl` is the only authority worth asking — an allowlist of IANA names goes\n * stale, and `toLocaleDateString` throws a RangeError on a name it does not\n * know, which would take down a composing page rather than mis-date one post.\n */\nexport function isSupportedTimeZone(value: unknown): boolean {\n const name = String(value ?? '').trim()\n if (!name) return false\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: name })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Every zone this runtime can offer as a choice (AGL-3252).\n *\n * Read off `Intl` rather than checked in, for the reason\n * {@link isSupportedTimeZone} asks `Intl` rather than an allowlist: the IANA\n * database renames zones, and a stored list would offer names the runtime has\n * stopped accepting while missing ones it has learned. A picker built from\n * this and a validator built from that cannot disagree about what is\n * choosable.\n *\n * A memoized FUNCTION rather than a module constant: this module is imported\n * by the tenant page composer, which never draws a picker, and enumerating\n * four hundred zone names on import is work nothing there asks for.\n *\n * An old runtime without `supportedValuesOf` gets an EMPTY list rather than a\n * broken control — the callers offer their inherited-default option first, so\n * a picker with no zones in it still says what the site is doing.\n */\nlet supportedTimeZonesCache: readonly string[] | null = null\nexport function supportedTimeZones(): readonly string[] {\n if (supportedTimeZonesCache) return supportedTimeZonesCache\n try {\n supportedTimeZonesCache =\n (Intl as { supportedValuesOf?: (key: string) => string[] })\n .supportedValuesOf?.('timeZone') ?? []\n } catch {\n supportedTimeZonesCache = []\n }\n return supportedTimeZonesCache\n}\n\n/**\n * The zone a site's dates are read in (AGL-3237, AGL-3252).\n *\n * THE SITE WINS, then the workspace, then UTC. The organization names the\n * zone once and every site it owns inherits it — which is the answer a\n * customer with one site wants and never has to think about — and a site that\n * names its own overrides it, for the agency running a Chicago site and a\n * Berlin site out of one workspace. Neither set is UTC: the value every\n * existing site already renders, so nothing moves by a day when this ships.\n *\n * Each candidate is validated in turn rather than trusted, and an unusable\n * one FALLS THROUGH rather than terminating the chain: a stored zone can\n * predate an IANA rename, or be whatever a hand-edited document holds, and a\n * site whose own zone has gone stale should read in its workspace's rather\n * than jump to UTC.\n *\n * ⚑ AGL-3237 deferred the site half, on the reading that a `timeZone` on the\n * host document would need a Firestore rules change to go with it. It did\n * not. The host block's client branch is a `hasAny([…])` DENY list, so a\n * field it does not name is already writable by an editor; what the override\n * needed was a classification in `HOST_CLIENT_WRITABLE_FIELDS`, which is a\n * code change on the same commit rather than a rules deploy. The deferral was\n * a misreading of the guard, not a cost.\n */\nexport function resolveSiteTimeZone(\n org?: { timeZone?: string } | null,\n host?: { timeZone?: string } | null,\n): string {\n const siteZone = String(host?.timeZone ?? '').trim()\n if (isSupportedTimeZone(siteZone)) return siteZone\n const orgZone = String(org?.timeZone ?? '').trim()\n return isSupportedTimeZone(orgZone) ? orgZone : COLLECTION_ENTRY_DATE_TIME_ZONE\n}\n\nexport function formatCollectionEntryDate(\n publishedAt: { seconds: number } | null | undefined,\n format?: CollectionEntryDateFormat,\n locale: string = COLLECTION_ENTRY_DATE_LOCALE,\n /**\n * The site's zone (AGL-3237) — resolved once per render and passed down, so\n * server and client format the same instant into the same string.\n */\n timeZoneName: string = COLLECTION_ENTRY_DATE_TIME_ZONE,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n // An unknown zone formats in UTC rather than throwing: this runs while a\n // page composes, and a RangeError here is a site that does not render.\n const timeZone = isSupportedTimeZone(timeZoneName)\n ? timeZoneName\n : COLLECTION_ENTRY_DATE_TIME_ZONE\n switch (normalizeCollectionEntryDateFormat(format)) {\n case 'monthYear':\n return date.toLocaleDateString(locale, {\n month: 'short',\n year: 'numeric',\n timeZone,\n })\n case 'mediumDate':\n return date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'longDate':\n return date.toLocaleDateString(locale, {\n month: 'long',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'iso': {\n // The calendar day IN THE SITE'S ZONE, not `toISOString()`'s slice by\n // luck and not the RUNTIME's local day: `getFullYear`/`getMonth`/\n // `getDate` read the host's zone, so this branch moved an entry by a\n // day depending on who rendered it. Read through the same formatter the\n // branches above use, so all four shapes name one day.\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(date)\n const part = (type: string) =>\n parts.find((entry) => entry.type === type)?.value ?? ''\n return `${part('year')}-${part('month')}-${part('day')}`\n }\n default:\n return date.toLocaleDateString(locale, { timeZone })\n }\n}\n\n/**\n * One entry's publish instant for a machine to read: an ISO 8601 date-time in\n * UTC, or `''` for an entry with no date (AGL-2956).\n *\n * {@link formatCollectionEntryDate} writes the date a READER sees, and every\n * one of its shapes is ambiguous or incomplete to a parser: `8/9/2026` is a\n * different day in `en-GB`, and even its `iso` shape is a calendar day with no\n * time or zone. A field that has to be parsed — a Video element's publication\n * date, which the page publishes as `VideoObject.uploadDate` — needs the\n * instant itself, spelled by `toISOString()` as `Article.datePublished` is.\n *\n * It reads the same `publishedAt` and treats a missing or zero `seconds` the\n * same way, so an entry is dated here exactly when it is dated there.\n *\n * A `seconds` no `Date` can hold answers `''` rather than throwing:\n * `toISOString` raises a RangeError on an invalid date, and this runs while a\n * page composes.\n */\nexport function collectionEntryPublishedAtIso(\n publishedAt: { seconds: number } | null | undefined,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString()\n}\n"],"names":["COLLECTION_ENTRY_DATE_FORMAT_DEFAULT","COLLECTION_ENTRY_DATE_FORMAT_OPTIONS","value","label","normalizeCollectionEntryDateFormat","wanted","String","trim","match","find","option","COLLECTION_ENTRY_DATE_LOCALE","COLLECTION_ENTRY_DATE_TIME_ZONE","isSupportedTimeZone","name","Intl","DateTimeFormat","timeZone","supportedTimeZonesCache","supportedTimeZones","supportedValuesOf","resolveSiteTimeZone","org","host","siteZone","orgZone","formatCollectionEntryDate","publishedAt","format","locale","timeZoneName","seconds","date","Date","toLocaleDateString","month","year","day","parts","formatToParts","part","type","entry","collectionEntryPublishedAtIso","Number","isNaN","getTime","toISOString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/collection-entry-date.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How an entry's published date is spelled, and the one function that spells\n * it.\n *\n * Its own module because a published page's client bundle reads\n * `formatCollectionEntryDate` and nothing else here: `collection-entries.ts`\n * carries the whole collection runtime — routing, pagination, category links,\n * the search index and the four block expanders — and a bundler cannot drop\n * the rest of it around a single named import.\n */\n\n/**\n * How an entry's published date reads (AGL-1459).\n *\n * Named shapes rather than a format string, for the same reason the styles\n * panel offers a unit picker rather than a CSS box: an editor choosing a\n * byline is not choosing a grammar, and a free-typed pattern is a field where\n * every typo renders as itself on a published page.\n */\nexport type CollectionEntryDateFormat =\n 'default' | 'monthYear' | 'mediumDate' | 'longDate' | 'iso'\n\n/**\n * \"However the site already renders it\" — a REAL value, never absence\n * (AGL-1459).\n *\n * `''` is the shape AGL-1451/AGL-1453 closed repo-wide: an emptied field\n * cannot survive a save, so an author who tried a format and wanted the\n * original back would have no option to pick. So the do-nothing choice is a\n * word, exactly like `COLLECTION_ALL_PILL_NONE` in `collection-entries.ts`.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_DEFAULT: CollectionEntryDateFormat =\n 'default'\n\n/**\n * The offered formats, with the labels an author reads (AGL-1459). Lives here\n * rather than in the block's schema so the list and the formatter cannot\n * drift into offering a shape nothing knows how to produce.\n *\n * Example dates in the labels are written the way the default runtime renders\n * them, which is what makes the choice legible without opening a preview.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_OPTIONS: readonly {\n value: CollectionEntryDateFormat\n label: string\n}[] = [\n { value: 'default', label: 'Site default — 8/9/2026' },\n { value: 'monthYear', label: 'Month and year — Aug 2026' },\n { value: 'mediumDate', label: 'Short date — Aug 9, 2026' },\n { value: 'longDate', label: 'Long date — August 9, 2026' },\n { value: 'iso', label: 'ISO — 2026-08-09' },\n]\n\n/** Any stored value read back as a format this module knows (AGL-1459). */\nexport function normalizeCollectionEntryDateFormat(\n value: unknown,\n): CollectionEntryDateFormat {\n const wanted = String(value ?? '').trim()\n const match = COLLECTION_ENTRY_DATE_FORMAT_OPTIONS.find(\n (option) => option.value === wanted,\n )\n return match?.value ?? COLLECTION_ENTRY_DATE_FORMAT_DEFAULT\n}\n\n/**\n * One entry's published date, in the shape the author asked for (AGL-1459).\n *\n * **The formatting lives HERE, beside the timestamp, and deliberately not in\n * the block.** By the time a date reaches the component it is already a\n * formatted string, and re-parsing one is ambiguous by construction: the same\n * `8/9/2026` reads as 9 August under an `en-US` runtime and 8 September under\n * `en-GB`. A byline that silently moves an article by three weeks is far worse\n * than one that is the wrong shape.\n *\n * `default` returns exactly `toLocaleDateString()` — the string this function\n * replaced, character for character — because the block is live on published\n * entries and opening a dropdown must not restyle them.\n *\n * ## Why the runtime gets no say (AGL-1926)\n *\n * Every branch pins BOTH the locale and the time zone, and the answer is a\n * pure function of the timestamp. It used to pass `locale` straight through\n * (normally `undefined`) with no `timeZone` at all, so the output was a\n * function of the RUNTIME: `en-US` + UTC on a Vercel server, the visitor's\n * own locale and zone in their browser. That is fine while only the server\n * ever calls it — the string is stamped into node props at compose time and\n * both sides then agree on it — but it makes the function a hydration\n * mismatch waiting for its first client-side caller, and `catch-all-client`\n * was exactly that caller. A post published at 02:30 UTC is dated the 10th by\n * the server and the 9th by every visitor west of Greenwich; React reports\n * the difference as a text mismatch (the live React #418 on tenant-web,\n * AGL-1926) and then reconciles against a DOM it no longer describes, which\n * is where the `removeChild`/`insertBefore` pair comes from.\n *\n * The pinned values are the ones production already emits, so no published\n * page changes: Vercel runs UTC with an `en-US` ICU default, which is why\n * every entry date served from aglyn.com today reads `8/9/2026`. Pinning\n * makes that byte-for-byte guaranteed instead of a property of the host.\n *\n * `locale` still exists for a caller that must render in a different one; it\n * now defaults to the value the server was picking implicitly rather than to\n * \"whatever this runtime happens to be\".\n */\nexport const COLLECTION_ENTRY_DATE_LOCALE = 'en-US'\n\n/**\n * The zone a calendar day is read in when a site has not named one.\n *\n * Entry timestamps are absolute instants; the day they are ATTRIBUTED to is\n * an editorial fact about the PUBLISHER, and `timeZone` below is how a site\n * states it. UTC remains the default because it is the answer every existing\n * site is already rendering — a site that never sets a zone must not have its\n * archive move by a day on deploy.\n *\n * ⚑ What AGL-1926 needed was not UTC, it was a zone BOTH SIDES AGREE ON. A\n * per-site zone is still that: it is resolved once, server-side, and travels\n * in the node props, so the client re-render reads the same string. What\n * would reopen the React #418 crash is reading the VISITOR's zone, because\n * only one side of the render knows it.\n */\nexport const COLLECTION_ENTRY_DATE_TIME_ZONE = 'UTC'\n\n/**\n * Is this a zone the runtime can actually format in (AGL-3237)?\n *\n * `Intl` is the only authority worth asking — an allowlist of IANA names goes\n * stale, and `toLocaleDateString` throws a RangeError on a name it does not\n * know, which would take down a composing page rather than mis-date one post.\n */\nexport function isSupportedTimeZone(value: unknown): boolean {\n const name = String(value ?? '').trim()\n if (!name) return false\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: name })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Every zone this runtime can offer as a choice (AGL-3252).\n *\n * Read off `Intl` rather than checked in, for the reason\n * {@link isSupportedTimeZone} asks `Intl` rather than an allowlist: the IANA\n * database renames zones, and a stored list would offer names the runtime has\n * stopped accepting while missing ones it has learned. A picker built from\n * this and a validator built from that cannot disagree about what is\n * choosable.\n *\n * A memoized FUNCTION rather than a module constant: this module is imported\n * by the tenant page composer, which never draws a picker, and enumerating\n * four hundred zone names on import is work nothing there asks for.\n *\n * An old runtime without `supportedValuesOf` gets an EMPTY list rather than a\n * broken control — the callers offer their inherited-default option first, so\n * a picker with no zones in it still says what the site is doing.\n */\nlet supportedTimeZonesCache: readonly string[] | null = null\nexport function supportedTimeZones(): readonly string[] {\n if (supportedTimeZonesCache) return supportedTimeZonesCache\n try {\n supportedTimeZonesCache =\n (Intl as { supportedValuesOf?: (key: string) => string[] })\n .supportedValuesOf?.('timeZone') ?? []\n } catch {\n supportedTimeZonesCache = []\n }\n return supportedTimeZonesCache\n}\n\n/**\n * The zone a site's dates are read in (AGL-3237, AGL-3252).\n *\n * THE SITE WINS, then the workspace, then UTC. The organization names the\n * zone once and every site it owns inherits it — which is the answer a\n * customer with one site wants and never has to think about — and a site that\n * names its own overrides it, for the agency running a Chicago site and a\n * Berlin site out of one workspace. Neither set is UTC: the value every\n * existing site already renders, so nothing moves by a day when this ships.\n *\n * Each candidate is validated in turn rather than trusted, and an unusable\n * one FALLS THROUGH rather than terminating the chain: a stored zone can\n * predate an IANA rename, or be whatever a hand-edited document holds, and a\n * site whose own zone has gone stale should read in its workspace's rather\n * than jump to UTC.\n *\n * ⚑ AGL-3237 deferred the site half, on the reading that a `timeZone` on the\n * host document would need a Firestore rules change to go with it. It did\n * not. The host block's client branch is a `hasAny([…])` DENY list, so a\n * field it does not name is already writable by an editor; what the override\n * needed was a classification in `HOST_CLIENT_WRITABLE_FIELDS`, which is a\n * code change on the same commit rather than a rules deploy. The deferral was\n * a misreading of the guard, not a cost.\n */\nexport function resolveSiteTimeZone(\n org?: { timeZone?: string } | null,\n host?: { timeZone?: string } | null,\n): string {\n const siteZone = String(host?.timeZone ?? '').trim()\n if (isSupportedTimeZone(siteZone)) return siteZone\n const orgZone = String(org?.timeZone ?? '').trim()\n return isSupportedTimeZone(orgZone) ? orgZone : COLLECTION_ENTRY_DATE_TIME_ZONE\n}\n\nexport function formatCollectionEntryDate(\n publishedAt: { seconds: number } | null | undefined,\n format?: CollectionEntryDateFormat,\n locale: string = COLLECTION_ENTRY_DATE_LOCALE,\n /**\n * The site's zone (AGL-3237) — resolved once per render and passed down, so\n * server and client format the same instant into the same string.\n */\n timeZoneName: string = COLLECTION_ENTRY_DATE_TIME_ZONE,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n // An unknown zone formats in UTC rather than throwing: this runs while a\n // page composes, and a RangeError here is a site that does not render.\n const timeZone = isSupportedTimeZone(timeZoneName)\n ? timeZoneName\n : COLLECTION_ENTRY_DATE_TIME_ZONE\n switch (normalizeCollectionEntryDateFormat(format)) {\n case 'monthYear':\n return date.toLocaleDateString(locale, {\n month: 'short',\n year: 'numeric',\n timeZone,\n })\n case 'mediumDate':\n return date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'longDate':\n return date.toLocaleDateString(locale, {\n month: 'long',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'iso': {\n // The calendar day IN THE SITE'S ZONE, not `toISOString()`'s slice by\n // luck and not the RUNTIME's local day: `getFullYear`/`getMonth`/\n // `getDate` read the host's zone, so this branch moved an entry by a\n // day depending on who rendered it. Read through the same formatter the\n // branches above use, so all four shapes name one day.\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(date)\n const part = (type: string) =>\n parts.find((entry) => entry.type === type)?.value ?? ''\n return `${part('year')}-${part('month')}-${part('day')}`\n }\n default:\n return date.toLocaleDateString(locale, { timeZone })\n }\n}\n\n/**\n * One entry's publish instant for a machine to read: an ISO 8601 date-time in\n * UTC, or `''` for an entry with no date (AGL-2956).\n *\n * {@link formatCollectionEntryDate} writes the date a READER sees, and every\n * one of its shapes is ambiguous or incomplete to a parser: `8/9/2026` is a\n * different day in `en-GB`, and even its `iso` shape is a calendar day with no\n * time or zone. A field that has to be parsed — a Video element's publication\n * date, which the page publishes as `VideoObject.uploadDate` — needs the\n * instant itself, spelled by `toISOString()` as `Article.datePublished` is.\n *\n * It reads the same `publishedAt` and treats a missing or zero `seconds` the\n * same way, so an entry is dated here exactly when it is dated there.\n *\n * A `seconds` no `Date` can hold answers `''` rather than throwing:\n * `toISOString` raises a RangeError on an invalid date, and this runs while a\n * page composes.\n */\nexport function collectionEntryPublishedAtIso(\n publishedAt: { seconds: number } | null | undefined,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString()\n}\n\n/**\n * The stored field the console's entries table sorts its Published column by\n * (AGL-3323): the instant {@link entryPublishSortStamp} names, kept on the\n * entry beside the two fields it is derived from.\n *\n * It exists because Firestore cannot sort on a fallback. `orderBy` reads one\n * field, and an entry that lacks it is not sorted last, it is left out of the\n * ordered read entirely. A scheduled entry has no `publishedAt` until it goes\n * live, so a table ordered on `publishedAt` could only append it after every\n * published entry — the last page, in both directions — while the cell above\n * it showed its `publishAt`. One stored field holding the date the cell shows\n * is the only shape an index can walk.\n *\n * It is a CONSOLE key. The public site's listings go on sorting and filtering\n * `publishedAt`, which only a live entry carries; this field must never become\n * what decides what a visitor sees, because a scheduled entry carries it too.\n */\nexport const ENTRY_PUBLISH_SORT_FIELD = 'publishSortAt'\n\n/**\n * The one instant an entry is listed under in the console (AGL-3206,\n * AGL-3323): its scheduled `publishAt` while it waits to go out, its\n * `publishedAt` otherwise, and `null` when it has neither.\n *\n * Scheduled wins over `publishedAt` because a SCHEDULED entry is not on the\n * site: scheduling a published entry takes it off the live set until the date\n * arrives, and it keeps the old `publishedAt` only until the runtime flips it\n * back and overwrites that with `publishAt`. The date it will go out on is\n * the true one for it.\n *\n * The `status` guard is load-bearing in the other direction too. A draft that\n * still holds a `publishAt` from a cancelled schedule is undated here rather\n * than sorting among the dated rows on a date it never displays.\n *\n * Every writer of `status`, `publishedAt` or `publishAt` stores this value\n * under {@link ENTRY_PUBLISH_SORT_FIELD} — and removes the field on `null` —\n * so the stored key and the rendered cell answer from the same rule.\n */\nexport function entryPublishSortStamp<T>(entry: {\n status?: unknown\n publishedAt?: T | null\n publishAt?: T | null\n} | null | undefined): T | null {\n if (entry?.status === 'scheduled' && entry.publishAt) return entry.publishAt\n return entry?.publishedAt ?? null\n}\n"],"names":["COLLECTION_ENTRY_DATE_FORMAT_DEFAULT","COLLECTION_ENTRY_DATE_FORMAT_OPTIONS","value","label","normalizeCollectionEntryDateFormat","wanted","String","trim","match","find","option","COLLECTION_ENTRY_DATE_LOCALE","COLLECTION_ENTRY_DATE_TIME_ZONE","isSupportedTimeZone","name","Intl","DateTimeFormat","timeZone","supportedTimeZonesCache","supportedTimeZones","supportedValuesOf","resolveSiteTimeZone","org","host","siteZone","orgZone","formatCollectionEntryDate","publishedAt","format","locale","timeZoneName","seconds","date","Date","toLocaleDateString","month","year","day","parts","formatToParts","part","type","entry","collectionEntryPublishedAtIso","Number","isNaN","getTime","toISOString","ENTRY_PUBLISH_SORT_FIELD","entryPublishSortStamp","status","publishAt"],"mappings":"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"}
|
|
@@ -176,6 +176,7 @@ export declare function datasetReferencedIds(model: DatasetModel, values: Record
|
|
|
176
176
|
*/
|
|
177
177
|
export declare function datasetIntegrityFields(model: DatasetModel, values: Record<string, unknown> | undefined): {
|
|
178
178
|
referencedIds?: string[];
|
|
179
|
+
filterKeys?: string[];
|
|
179
180
|
};
|
|
180
181
|
/**
|
|
181
182
|
* `datasetIntegrityFields` for a MERGING write (`update`, `set` with `merge`
|
|
@@ -187,8 +188,101 @@ export declare function datasetIntegrityFields(model: DatasetModel, values: Reco
|
|
|
187
188
|
* at — a `restrict` that refuses a delete nothing is holding. `clear` is the
|
|
188
189
|
* caller's delete sentinel: `deleteField()` in the web SDK,
|
|
189
190
|
* `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported
|
|
190
|
-
* because this module is shared by both and must stay SDK-free.
|
|
191
|
+
* 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
|
+
* value stops answering the filters its old values did.
|
|
191
194
|
*/
|
|
192
195
|
export declare function datasetIntegrityUpdate<TClear>(model: DatasetModel, values: Record<string, unknown> | undefined, clear: TClear): {
|
|
193
196
|
referencedIds: string[] | TClear;
|
|
197
|
+
filterKeys: string[] | TClear;
|
|
194
198
|
};
|
|
199
|
+
/**
|
|
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.
|
|
203
|
+
*
|
|
204
|
+
* ## Why a token array, and not the values
|
|
205
|
+
*
|
|
206
|
+
* A record's fields live under `values`, a user-defined, unbounded map that
|
|
207
|
+
* the index configuration exempts from indexing — auto-indexing it can exceed
|
|
208
|
+
* Firestore's per-document index-entry limit — so `where('values.<field>', …)`
|
|
209
|
+
* is refused. A composite index per dataset would need an admin call per
|
|
210
|
+
* dataset, runs into the project's composite-index cap, and is deleted by the
|
|
211
|
+
* next `firestore:indexes` deploy of a file that does not list it.
|
|
212
|
+
*
|
|
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.
|
|
217
|
+
*
|
|
218
|
+
* ## The token grammar
|
|
219
|
+
*
|
|
220
|
+
* `f:<fieldId>=<value>` a field equals a value. Enum (a text field with
|
|
221
|
+
* `validation.options`) values are kept exactly;
|
|
222
|
+
* plain text is lower-cased, trimmed and clipped to
|
|
223
|
+
* {@link DATASET_FILTER_VALUE_MAX} characters;
|
|
224
|
+
* booleans are `true`/`false`; numbers are
|
|
225
|
+
* `String(number)`; a `sorted` list contributes one
|
|
226
|
+
* token per member.
|
|
227
|
+
* `f:<fieldId>^<prefix>` a word of a plain text field starts with this.
|
|
228
|
+
* `s:<prefix>` a word of ANY text value (enums and list members
|
|
229
|
+
* included) starts with this — the quick search.
|
|
230
|
+
*
|
|
231
|
+
* Timestamps, references, maps, bytes, coordinates and nil fields carry no
|
|
232
|
+
* field tokens: a timestamp is asked in ranges, which one token cannot serve,
|
|
233
|
+
* and the rest are not asked about by value.
|
|
234
|
+
*
|
|
235
|
+
* The array is sorted, de-duplicated and capped at
|
|
236
|
+
* {@link DATASET_FILTER_KEYS_MAX}. Past the cap, the `=` tokens are kept
|
|
237
|
+
* first, then the per-field word prefixes, then the search prefixes, so a
|
|
238
|
+
* very long record loses search reach before it loses exact matches.
|
|
239
|
+
*/
|
|
240
|
+
export declare const DATASET_FILTER_KEYS_MAX = 500;
|
|
241
|
+
/** Plain text values are clipped to this many characters in an `=` token. */
|
|
242
|
+
export declare const DATASET_FILTER_VALUE_MAX = 64;
|
|
243
|
+
/** Word prefixes run from 1 to this many characters. */
|
|
244
|
+
export declare const DATASET_FILTER_PREFIX_MAX = 12;
|
|
245
|
+
/** Only the first this-many words of a value contribute prefixes. */
|
|
246
|
+
export declare const DATASET_FILTER_WORDS_MAX = 40;
|
|
247
|
+
/** One clause of the records table's filter: `{ field, op, value }`. */
|
|
248
|
+
export interface DatasetFilterClause {
|
|
249
|
+
field: string;
|
|
250
|
+
op: string;
|
|
251
|
+
value: string;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The words of a text value: split on anything that is not a letter or a
|
|
255
|
+
* digit, lower-cased, at most {@link DATASET_FILTER_WORDS_MAX} of them.
|
|
256
|
+
*/
|
|
257
|
+
export declare function datasetFilterWords(text: string): string[];
|
|
258
|
+
/** The tokens described in {@link DATASET_FILTER_KEYS_MAX}'s comment. */
|
|
259
|
+
export declare function datasetFilterKeys(model: DatasetModel, values: Record<string, unknown> | undefined): string[];
|
|
260
|
+
/**
|
|
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.
|
|
263
|
+
*
|
|
264
|
+
* - enum, boolean, number and list `equals` → the `=` token of the value;
|
|
265
|
+
* - plain text `equals` → the lower-cased `=` token;
|
|
266
|
+
* - 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;
|
|
268
|
+
* - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.
|
|
269
|
+
*
|
|
270
|
+
* `clause.field` is the field id. The operator names are the grid's: a
|
|
271
|
+
* select's `equals`, a number's `=`, a boolean's `is`, a list's `contains`.
|
|
272
|
+
*/
|
|
273
|
+
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
|
+
/**
|
|
283
|
+
* The quick-search token for one typed word, or null for a word with no
|
|
284
|
+
* letters or digits. A word with punctuation inside it is asked by its first
|
|
285
|
+
* run — split the search text with {@link datasetFilterWords} to ask for
|
|
286
|
+
* every run.
|
|
287
|
+
*/
|
|
288
|
+
export declare function datasetSearchToken(word: string): string | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -384,9 +385,16 @@ const isFiniteNumber = (value)=>typeof value === 'number' && Number.isFinite(val
|
|
|
384
385
|
* where clearing the last reference has to remove the field.
|
|
385
386
|
*/ export function datasetIntegrityFields(model, values) {
|
|
386
387
|
const referencedIds = datasetReferencedIds(model, values);
|
|
387
|
-
|
|
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`.
|
|
392
|
+
const filterKeys = datasetFilterKeys(model, values);
|
|
393
|
+
return _extends({}, referencedIds.length ? {
|
|
388
394
|
referencedIds
|
|
389
|
-
} : {}
|
|
395
|
+
} : {}, filterKeys.length ? {
|
|
396
|
+
filterKeys
|
|
397
|
+
} : {});
|
|
390
398
|
}
|
|
391
399
|
/**
|
|
392
400
|
* `datasetIntegrityFields` for a MERGING write (`update`, `set` with `merge`
|
|
@@ -398,12 +406,233 @@ const isFiniteNumber = (value)=>typeof value === 'number' && Number.isFinite(val
|
|
|
398
406
|
* at — a `restrict` that refuses a delete nothing is holding. `clear` is the
|
|
399
407
|
* caller's delete sentinel: `deleteField()` in the web SDK,
|
|
400
408
|
* `FieldValue.delete()` in the Admin SDK. It is passed in rather than imported
|
|
401
|
-
* because this module is shared by both and must stay SDK-free.
|
|
409
|
+
* 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
|
|
411
|
+
* value stops answering the filters its old values did.
|
|
402
412
|
*/ export function datasetIntegrityUpdate(model, values, clear) {
|
|
403
413
|
const referencedIds = datasetReferencedIds(model, values);
|
|
414
|
+
const filterKeys = datasetFilterKeys(model, values);
|
|
404
415
|
return {
|
|
405
|
-
referencedIds: referencedIds.length ? referencedIds : clear
|
|
416
|
+
referencedIds: referencedIds.length ? referencedIds : clear,
|
|
417
|
+
filterKeys: filterKeys.length ? filterKeys : clear
|
|
406
418
|
};
|
|
407
419
|
}
|
|
420
|
+
/**
|
|
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.
|
|
424
|
+
*
|
|
425
|
+
* ## Why a token array, and not the values
|
|
426
|
+
*
|
|
427
|
+
* A record's fields live under `values`, a user-defined, unbounded map that
|
|
428
|
+
* the index configuration exempts from indexing — auto-indexing it can exceed
|
|
429
|
+
* Firestore's per-document index-entry limit — so `where('values.<field>', …)`
|
|
430
|
+
* is refused. A composite index per dataset would need an admin call per
|
|
431
|
+
* dataset, runs into the project's composite-index cap, and is deleted by the
|
|
432
|
+
* next `firestore:indexes` deploy of a file that does not list it.
|
|
433
|
+
*
|
|
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.
|
|
438
|
+
*
|
|
439
|
+
* ## The token grammar
|
|
440
|
+
*
|
|
441
|
+
* `f:<fieldId>=<value>` a field equals a value. Enum (a text field with
|
|
442
|
+
* `validation.options`) values are kept exactly;
|
|
443
|
+
* plain text is lower-cased, trimmed and clipped to
|
|
444
|
+
* {@link DATASET_FILTER_VALUE_MAX} characters;
|
|
445
|
+
* booleans are `true`/`false`; numbers are
|
|
446
|
+
* `String(number)`; a `sorted` list contributes one
|
|
447
|
+
* token per member.
|
|
448
|
+
* `f:<fieldId>^<prefix>` a word of a plain text field starts with this.
|
|
449
|
+
* `s:<prefix>` a word of ANY text value (enums and list members
|
|
450
|
+
* included) starts with this — the quick search.
|
|
451
|
+
*
|
|
452
|
+
* Timestamps, references, maps, bytes, coordinates and nil fields carry no
|
|
453
|
+
* field tokens: a timestamp is asked in ranges, which one token cannot serve,
|
|
454
|
+
* and the rest are not asked about by value.
|
|
455
|
+
*
|
|
456
|
+
* The array is sorted, de-duplicated and capped at
|
|
457
|
+
* {@link DATASET_FILTER_KEYS_MAX}. Past the cap, the `=` tokens are kept
|
|
458
|
+
* first, then the per-field word prefixes, then the search prefixes, so a
|
|
459
|
+
* very long record loses search reach before it loses exact matches.
|
|
460
|
+
*/ export const DATASET_FILTER_KEYS_MAX = 500;
|
|
461
|
+
/** Plain text values are clipped to this many characters in an `=` token. */ export const DATASET_FILTER_VALUE_MAX = 64;
|
|
462
|
+
/** Word prefixes run from 1 to this many characters. */ export const DATASET_FILTER_PREFIX_MAX = 12;
|
|
463
|
+
/** Only the first this-many words of a value contribute prefixes. */ export const DATASET_FILTER_WORDS_MAX = 40;
|
|
464
|
+
/**
|
|
465
|
+
* The words of a text value: split on anything that is not a letter or a
|
|
466
|
+
* digit, lower-cased, at most {@link DATASET_FILTER_WORDS_MAX} of them.
|
|
467
|
+
*/ export function datasetFilterWords(text) {
|
|
468
|
+
return String(text).toLowerCase().split(RegExp("[^\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+", "u")).filter(Boolean).slice(0, DATASET_FILTER_WORDS_MAX);
|
|
469
|
+
}
|
|
470
|
+
/** A word's prefixes, 1 to {@link DATASET_FILTER_PREFIX_MAX} characters. */ function prefixes(word) {
|
|
471
|
+
const chars = Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX);
|
|
472
|
+
return chars.map((_char, at)=>chars.slice(0, at + 1).join(''));
|
|
473
|
+
}
|
|
474
|
+
/** A word clipped the way its stored prefixes are. */ function clipWord(word) {
|
|
475
|
+
return Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX).join('');
|
|
476
|
+
}
|
|
477
|
+
const isEnum = (field)=>{
|
|
478
|
+
var _ref;
|
|
479
|
+
var _field_validation_options, _field_validation;
|
|
480
|
+
return field.type === 'text' && ((_ref = (_field_validation = field.validation) == null ? void 0 : (_field_validation_options = _field_validation.options) == null ? void 0 : _field_validation_options.length) != null ? _ref : 0) > 0;
|
|
481
|
+
};
|
|
482
|
+
const isNumeric = (field)=>field.type === 'int32' || field.type === 'int64' || field.type === 'float';
|
|
483
|
+
/**
|
|
484
|
+
* A stored value as text, or null. Numbers are accepted because a form or an
|
|
485
|
+
* automation stores every value as text and an import may do the reverse.
|
|
486
|
+
*/ function asText(value) {
|
|
487
|
+
if (typeof value === 'string') return value;
|
|
488
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
/** Plain text as its `=` token's value: lower-cased, trimmed, clipped. */ function textKey(value) {
|
|
492
|
+
return Array.from(value.trim().toLowerCase()).slice(0, DATASET_FILTER_VALUE_MAX).join('');
|
|
493
|
+
}
|
|
494
|
+
/** A stored number (or its text form) as its `=` token's value. */ function numberKey(value) {
|
|
495
|
+
const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
|
|
496
|
+
return Number.isFinite(number) ? String(number) : null;
|
|
497
|
+
}
|
|
498
|
+
/** A stored boolean (or its text form) as its `=` token's value. */ function boolKey(value) {
|
|
499
|
+
if (typeof value === 'boolean') return String(value);
|
|
500
|
+
if (value === 'true' || value === 'false') return value;
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
/** Every field a model declares, in display order and then the rest. */ function modelFieldIds(model) {
|
|
504
|
+
var _model_order, _model_fields;
|
|
505
|
+
return [
|
|
506
|
+
...new Set([
|
|
507
|
+
...(_model_order = model.order) != null ? _model_order : [],
|
|
508
|
+
...Object.keys((_model_fields = model.fields) != null ? _model_fields : {})
|
|
509
|
+
])
|
|
510
|
+
];
|
|
511
|
+
}
|
|
512
|
+
/** The tokens described in {@link DATASET_FILTER_KEYS_MAX}'s comment. */ export function datasetFilterKeys(model, values) {
|
|
513
|
+
const exact = [];
|
|
514
|
+
const fieldWords = [];
|
|
515
|
+
const search = [];
|
|
516
|
+
const searchable = (text)=>{
|
|
517
|
+
for (const word of datasetFilterWords(text)){
|
|
518
|
+
for (const prefix of prefixes(word))search.push(`s:${prefix}`);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
for (const fieldId of modelFieldIds(model)){
|
|
522
|
+
var _model_fields;
|
|
523
|
+
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
|
|
524
|
+
const stored = values == null ? void 0 : values[fieldId];
|
|
525
|
+
if (!field || stored == null) continue;
|
|
526
|
+
if (field.type === 'text') {
|
|
527
|
+
const text = asText(stored);
|
|
528
|
+
if (text == null || !text.trim()) continue;
|
|
529
|
+
if (isEnum(field)) {
|
|
530
|
+
exact.push(`f:${fieldId}=${text}`);
|
|
531
|
+
} else {
|
|
532
|
+
exact.push(`f:${fieldId}=${textKey(text)}`);
|
|
533
|
+
for (const word of datasetFilterWords(text)){
|
|
534
|
+
for (const prefix of prefixes(word)){
|
|
535
|
+
fieldWords.push(`f:${fieldId}^${prefix}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
searchable(text);
|
|
540
|
+
} else if (field.type === 'bool') {
|
|
541
|
+
const key = boolKey(stored);
|
|
542
|
+
if (key) exact.push(`f:${fieldId}=${key}`);
|
|
543
|
+
} else if (isNumeric(field)) {
|
|
544
|
+
const key = numberKey(stored);
|
|
545
|
+
if (key) exact.push(`f:${fieldId}=${key}`);
|
|
546
|
+
} else if (field.type === 'sorted') {
|
|
547
|
+
for (const member of Array.isArray(stored) ? stored : [
|
|
548
|
+
stored
|
|
549
|
+
]){
|
|
550
|
+
var _asText;
|
|
551
|
+
const text = (_asText = asText(member)) == null ? void 0 : _asText.trim();
|
|
552
|
+
if (!text) continue;
|
|
553
|
+
exact.push(`f:${fieldId}=${text}`);
|
|
554
|
+
searchable(text);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const kept = [
|
|
559
|
+
...new Set([
|
|
560
|
+
...exact,
|
|
561
|
+
...fieldWords,
|
|
562
|
+
...search
|
|
563
|
+
])
|
|
564
|
+
].slice(0, DATASET_FILTER_KEYS_MAX);
|
|
565
|
+
return kept.sort();
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
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.
|
|
570
|
+
*
|
|
571
|
+
* - enum, boolean, number and list `equals` → the `=` token of the value;
|
|
572
|
+
* - plain text `equals` → the lower-cased `=` token;
|
|
573
|
+
* - 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;
|
|
575
|
+
* - everything else — ranges, dates, `isEmpty`, `isAnyOf`, negations — null.
|
|
576
|
+
*
|
|
577
|
+
* `clause.field` is the field id. The operator names are the grid's: a
|
|
578
|
+
* select's `equals`, a number's `=`, a boolean's `is`, a list's `contains`.
|
|
579
|
+
*/ export function datasetFilterToken(model, clause) {
|
|
580
|
+
var _clause_value;
|
|
581
|
+
var _model_fields;
|
|
582
|
+
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[clause.field];
|
|
583
|
+
if (!field) return null;
|
|
584
|
+
const { op } = clause;
|
|
585
|
+
const value = String((_clause_value = clause.value) != null ? _clause_value : '');
|
|
586
|
+
if (!value.trim()) return null;
|
|
587
|
+
const equals = op === 'equals' || op === '=' || op === 'is';
|
|
588
|
+
if (field.type === 'text') {
|
|
589
|
+
if (isEnum(field)) return equals ? `f:${clause.field}=${value}` : null;
|
|
590
|
+
if (equals) return `f:${clause.field}=${textKey(value)}`;
|
|
591
|
+
if (op === 'contains') {
|
|
592
|
+
const [first] = datasetFilterWords(value);
|
|
593
|
+
return first ? `f:${clause.field}^${clipWord(first)}` : null;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
if (field.type === 'bool') {
|
|
598
|
+
const key = boolKey(value.trim());
|
|
599
|
+
return equals && key ? `f:${clause.field}=${key}` : null;
|
|
600
|
+
}
|
|
601
|
+
if (isNumeric(field)) {
|
|
602
|
+
const key = numberKey(value);
|
|
603
|
+
return equals && key ? `f:${clause.field}=${key}` : null;
|
|
604
|
+
}
|
|
605
|
+
if (field.type === 'sorted') {
|
|
606
|
+
return equals || op === 'contains' ? `f:${clause.field}=${value.trim()}` : null;
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
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
|
+
/**
|
|
629
|
+
* The quick-search token for one typed word, or null for a word with no
|
|
630
|
+
* letters or digits. A word with punctuation inside it is asked by its first
|
|
631
|
+
* run — split the search text with {@link datasetFilterWords} to ask for
|
|
632
|
+
* every run.
|
|
633
|
+
*/ export function datasetSearchToken(word) {
|
|
634
|
+
const [first] = datasetFilterWords(word);
|
|
635
|
+
return first ? `s:${clipWord(first)}` : null;
|
|
636
|
+
}
|
|
408
637
|
|
|
409
638
|
//# sourceMappingURL=dataset-models.js.map
|