@aglyn/tenant-feature-instance 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +3 -0
- package/jest.config.d.ts +36 -0
- package/package.json +45 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
- package/src/lib/components/duplicate-resource-dialog.js +217 -0
- package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
- package/src/lib/constants/firebase-config.d.ts +68 -0
- package/src/lib/constants/firebase-config.js +155 -0
- package/src/lib/constants/firebase-config.js.map +1 -0
- package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
- package/src/lib/constants/firebase-emulator-hosts.js +81 -0
- package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
- package/src/lib/constants/mock-data.d.ts +24 -0
- package/src/lib/constants/mock-data.js +49 -0
- package/src/lib/constants/mock-data.js.map +1 -0
- package/src/lib/constants/site-paths.d.ts +47 -0
- package/src/lib/constants/site-paths.js +66 -0
- package/src/lib/constants/site-paths.js.map +1 -0
- package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
- package/src/lib/hooks/firebase/auth-persistence.js +117 -0
- package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
- package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
- package/src/lib/hooks/firebase/firebase-services.js +522 -0
- package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
- package/src/lib/hooks/firebase/firestore-cache.js +152 -0
- package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
- package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
- package/src/lib/hooks/firestore-denial-reporter.js +293 -0
- package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
- package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
- package/src/lib/hooks/helpers/listen-options.js +92 -0
- package/src/lib/hooks/helpers/listen-options.js.map +1 -0
- package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
- package/src/lib/hooks/helpers/use-doc.js +213 -0
- package/src/lib/hooks/helpers/use-doc.js.map +1 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
- package/src/lib/hooks/host-collection-queries.d.ts +125 -0
- package/src/lib/hooks/host-collection-queries.js +126 -0
- package/src/lib/hooks/host-collection-queries.js.map +1 -0
- package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
- package/src/lib/hooks/list-filter-constraints.js +288 -0
- package/src/lib/hooks/list-filter-constraints.js.map +1 -0
- package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
- package/src/lib/hooks/sorted-collection-window.js +150 -0
- package/src/lib/hooks/sorted-collection-window.js.map +1 -0
- package/src/lib/hooks/use-component-version.d.ts +43 -0
- package/src/lib/hooks/use-component-version.js +73 -0
- package/src/lib/hooks/use-component-version.js.map +1 -0
- package/src/lib/hooks/use-component.d.ts +39 -0
- package/src/lib/hooks/use-component.js +55 -0
- package/src/lib/hooks/use-component.js.map +1 -0
- package/src/lib/hooks/use-console-host-route.d.ts +24 -0
- package/src/lib/hooks/use-console-host-route.js +77 -0
- package/src/lib/hooks/use-console-host-route.js.map +1 -0
- package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
- package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
- package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
- package/src/lib/hooks/use-firestore-collection.js +307 -0
- package/src/lib/hooks/use-firestore-collection.js.map +1 -0
- package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
- package/src/lib/hooks/use-firestore-doc.js +192 -0
- package/src/lib/hooks/use-firestore-doc.js.map +1 -0
- package/src/lib/hooks/use-form-version.d.ts +44 -0
- package/src/lib/hooks/use-form-version.js +73 -0
- package/src/lib/hooks/use-form-version.js.map +1 -0
- package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
- package/src/lib/hooks/use-host-activity-logger.js +96 -0
- package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
- package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
- package/src/lib/hooks/use-host-campaigns.js +93 -0
- package/src/lib/hooks/use-host-campaigns.js.map +1 -0
- package/src/lib/hooks/use-host-org-id.d.ts +96 -0
- package/src/lib/hooks/use-host-org-id.js +154 -0
- package/src/lib/hooks/use-host-org-id.js.map +1 -0
- package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-host-resource-api.js +60 -0
- package/src/lib/hooks/use-host-resource-api.js.map +1 -0
- package/src/lib/hooks/use-host-template.d.ts +44 -0
- package/src/lib/hooks/use-host-template.js +82 -0
- package/src/lib/hooks/use-host-template.js.map +1 -0
- package/src/lib/hooks/use-host-version-api.d.ts +59 -0
- package/src/lib/hooks/use-host-version-api.js +69 -0
- package/src/lib/hooks/use-host-version-api.js.map +1 -0
- package/src/lib/hooks/use-host.d.ts +101 -0
- package/src/lib/hooks/use-host.js +45 -0
- package/src/lib/hooks/use-host.js.map +1 -0
- package/src/lib/hooks/use-layout-version.d.ts +33 -0
- package/src/lib/hooks/use-layout-version.js +64 -0
- package/src/lib/hooks/use-layout-version.js.map +1 -0
- package/src/lib/hooks/use-layout.d.ts +41 -0
- package/src/lib/hooks/use-layout.js +49 -0
- package/src/lib/hooks/use-layout.js.map +1 -0
- package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
- package/src/lib/hooks/use-live-artifact-count.js +102 -0
- package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +57 -0
- package/src/lib/hooks/use-org-member-options.js +112 -0
- package/src/lib/hooks/use-org-member-options.js.map +1 -0
- package/src/lib/hooks/use-org-plan.d.ts +46 -0
- package/src/lib/hooks/use-org-plan.js +86 -0
- package/src/lib/hooks/use-org-plan.js.map +1 -0
- package/src/lib/hooks/use-paged-collection.d.ts +95 -0
- package/src/lib/hooks/use-paged-collection.js +130 -0
- package/src/lib/hooks/use-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-plugin-config.d.ts +38 -0
- package/src/lib/hooks/use-plugin-config.js +120 -0
- package/src/lib/hooks/use-plugin-config.js.map +1 -0
- package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
- package/src/lib/hooks/use-scope-tokens.js +100 -0
- package/src/lib/hooks/use-scope-tokens.js.map +1 -0
- package/src/lib/hooks/use-screen-version.d.ts +33 -0
- package/src/lib/hooks/use-screen-version.js +65 -0
- package/src/lib/hooks/use-screen-version.js.map +1 -0
- package/src/lib/hooks/use-screen.d.ts +68 -0
- package/src/lib/hooks/use-screen.js +50 -0
- package/src/lib/hooks/use-screen.js.map +1 -0
- package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
- package/src/lib/hooks/use-sending-identity-api.js +95 -0
- package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
- package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
- package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
- package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
- package/src/lib/hooks/use-switcher-collection.js +275 -0
- package/src/lib/hooks/use-switcher-collection.js.map +1 -0
- package/src/lib/hooks/use-user-name.d.ts +51 -0
- package/src/lib/hooks/use-user-name.js +90 -0
- package/src/lib/hooks/use-user-name.js.map +1 -0
- package/src/lib/hooks/use-user-photo.d.ts +50 -0
- package/src/lib/hooks/use-user-photo.js +87 -0
- package/src/lib/hooks/use-user-photo.js.map +1 -0
- package/src/lib/tenant.d.ts +61 -0
- package/src/lib/tenant.js +62 -0
- package/src/lib/tenant.js.map +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { documentId, limit, orderBy, query } from "firebase/firestore";
|
|
17
|
+
/**
|
|
18
|
+
* ONE ordering decision for the console lists a plugin renders (AGL-2501).
|
|
19
|
+
*
|
|
20
|
+
* ## What an unordered `limit()` actually answers
|
|
21
|
+
*
|
|
22
|
+
* Firestore answers a capped query with no `orderBy` in DOCUMENT-ID order, and
|
|
23
|
+
* every collection these cards read is keyed by a generated `createResourceUid`
|
|
24
|
+
* or by `add()`. So `limit(100)` is not "the first hundred variables" — it is a
|
|
25
|
+
* pseudo-random hundred, and the rows past it are not merely unrendered, they
|
|
26
|
+
* are UNREACHABLE, because nothing shows them and nothing asks for more. A
|
|
27
|
+
* client `.sort()` over that sample is what hides it: the rows on screen run in
|
|
28
|
+
* a believable order, they are simply the wrong rows, and the ones missing
|
|
29
|
+
* leave no gap to notice.
|
|
30
|
+
*
|
|
31
|
+
* The console's site-artifact lists reached this answer first
|
|
32
|
+
* (`hostArtifactQuery`) and could not share it: an app cannot be imported from
|
|
33
|
+
* a library, so every plugin card faced the same question alone and eleven of
|
|
34
|
+
* them answered it by not ordering at all. This is that decision, where a
|
|
35
|
+
* plugin can ask for it.
|
|
36
|
+
*
|
|
37
|
+
* ## Why the walk orders on the document ID, and not on a field
|
|
38
|
+
*
|
|
39
|
+
* The obvious fix is `orderBy` on the field the list is sorted by — `name` for
|
|
40
|
+
* most of these. It cannot be used. `orderBy` matches only documents that HAVE
|
|
41
|
+
* the field, so ordering on one that any writer omits does not mis-order the
|
|
42
|
+
* list, it HIDES rows from it: a worse failure than the one being fixed, and a
|
|
43
|
+
* silent one.
|
|
44
|
+
*
|
|
45
|
+
* Every collection these cards read has such a writer:
|
|
46
|
+
*
|
|
47
|
+
* * `/api/hosts/resources` stores an ALLOW-LIST of fields and validates none
|
|
48
|
+
* of them for presence, so a create that sends no name produces a document
|
|
49
|
+
* with no `name` at all.
|
|
50
|
+
* * `/api/hosts/import` copies only the keys a bundle actually carries through
|
|
51
|
+
* `IMPORTABLE_FIELDS` — `variables`, `functions`, `workflows` and `actions`
|
|
52
|
+
* are all in it — so a restored document carries a name only if the exported
|
|
53
|
+
* one had it.
|
|
54
|
+
* * `createdAt`/`updatedAt` are no better: the resources route stamps them
|
|
55
|
+
* server-side and an import's `cleanDoc` stamps `updatedAt` only.
|
|
56
|
+
*
|
|
57
|
+
* A document's NAME is not a field and cannot be absent, so ordering on it
|
|
58
|
+
* drops nothing and the walk is TOTAL: every row is reachable by paging, which
|
|
59
|
+
* is the property the old queries lacked. It is not insertion order — the ids
|
|
60
|
+
* are random — so no list built on it may claim to be in one. What it is is
|
|
61
|
+
* stable, complete, and the same on every load.
|
|
62
|
+
*
|
|
63
|
+
* ## Callers must not re-sort the page
|
|
64
|
+
*
|
|
65
|
+
* Re-sorting a window of an id-ordered walk by name is the same lie the old
|
|
66
|
+
* code told: rows would run in one order within a page and another across
|
|
67
|
+
* pages, and the first page would still not be the alphabetical first page. A
|
|
68
|
+
* caller that holds a whole CEILING may sort — see `ceilingedWindow` — because
|
|
69
|
+
* it is sorting the entire collection rather than a slice of it.
|
|
70
|
+
*
|
|
71
|
+
* ## Why a `Query` is accepted and not only a `CollectionReference`
|
|
72
|
+
*
|
|
73
|
+
* A list is often a FILTERED collection — one kind of conversion, one channel
|
|
74
|
+
* — and a caller holding equality predicates could not reach this ordering
|
|
75
|
+
* without rebuilding it, which is how a second, unordered `limit()` comes to
|
|
76
|
+
* be written beside the one this module exists to replace. `query()` composes
|
|
77
|
+
* onto either, and a `CollectionReference` IS a `Query`, so widening the
|
|
78
|
+
* parameter adds no behavior: the walk is still ordered on the document name
|
|
79
|
+
* and is still total over the matching documents.
|
|
80
|
+
*
|
|
81
|
+
* Equality predicates only. A range filter or a second ordering would fight
|
|
82
|
+
* the `orderBy(documentId())` applied here — Firestore requires the range
|
|
83
|
+
* field to be ordered first — and such a query fails loudly at the listener
|
|
84
|
+
* rather than quietly returning the wrong rows.
|
|
85
|
+
*/ export function collectionPage(ref, pageLimit) {
|
|
86
|
+
return query(ref, orderBy(documentId()), limit(pageLimit));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The same ordering, for a list that is BOUNDED rather than paged.
|
|
90
|
+
*
|
|
91
|
+
* Asks for one document more than the ceiling, which is what turns "there is
|
|
92
|
+
* more than this" into a fact. Pair it with `ceilingedWindow` to drop the probe
|
|
93
|
+
* row before rendering.
|
|
94
|
+
*/ export function collectionCeiling(ref, ceiling) {
|
|
95
|
+
return query(ref, orderBy(documentId()), limit(ceiling + 1));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A read that is bounded but not paged, and can say when it is short.
|
|
99
|
+
*
|
|
100
|
+
* Some lists cannot be windowed by document and must still be bounded: a
|
|
101
|
+
* hierarchy sliced by row separates a child from its parent, a bundle
|
|
102
|
+
* straddling a page renders twice and partial each time, a precedence list
|
|
103
|
+
* paged by ten cannot swap its tenth row with its eleventh, and a table
|
|
104
|
+
* assembled from two collections cannot dedupe across a page boundary.
|
|
105
|
+
*
|
|
106
|
+
* A bare ceiling is silent, which is the half that matters. Every consequence
|
|
107
|
+
* on those surfaces is computed from what was read, so a collection above the
|
|
108
|
+
* ceiling is drawn as a whole one with nothing to distinguish "there is no such
|
|
109
|
+
* row" from "this card did not read it".
|
|
110
|
+
*
|
|
111
|
+
* Asking for ONE document more than the ceiling turns that into a fact for the
|
|
112
|
+
* price of a single read. The probe is never handed on: a caller that rendered
|
|
113
|
+
* `ceiling + 1` rows would be describing a window it did not draw.
|
|
114
|
+
*
|
|
115
|
+
* `hasMore` in `usePagedCollection` is the same trick for a paged list, and for
|
|
116
|
+
* the same reason — a comparison against the cap is wrong exactly when the
|
|
117
|
+
* count is an even multiple of it.
|
|
118
|
+
*/ export function ceilingedWindow(read, ceiling) {
|
|
119
|
+
const rows = read != null ? read : [];
|
|
120
|
+
return {
|
|
121
|
+
rows: rows.slice(0, ceiling),
|
|
122
|
+
truncated: rows.length > ceiling
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=host-collection-queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/host-collection-queries.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 {\n documentId,\n limit,\n orderBy,\n query,\n type CollectionReference,\n type Query,\n} from 'firebase/firestore'\n\n/**\n * ONE ordering decision for the console lists a plugin renders (AGL-2501).\n *\n * ## What an unordered `limit()` actually answers\n *\n * Firestore answers a capped query with no `orderBy` in DOCUMENT-ID order, and\n * every collection these cards read is keyed by a generated `createResourceUid`\n * or by `add()`. So `limit(100)` is not \"the first hundred variables\" — it is a\n * pseudo-random hundred, and the rows past it are not merely unrendered, they\n * are UNREACHABLE, because nothing shows them and nothing asks for more. A\n * client `.sort()` over that sample is what hides it: the rows on screen run in\n * a believable order, they are simply the wrong rows, and the ones missing\n * leave no gap to notice.\n *\n * The console's site-artifact lists reached this answer first\n * (`hostArtifactQuery`) and could not share it: an app cannot be imported from\n * a library, so every plugin card faced the same question alone and eleven of\n * them answered it by not ordering at all. This is that decision, where a\n * plugin can ask for it.\n *\n * ## Why the walk orders on the document ID, and not on a field\n *\n * The obvious fix is `orderBy` on the field the list is sorted by — `name` for\n * most of these. It cannot be used. `orderBy` matches only documents that HAVE\n * the field, so ordering on one that any writer omits does not mis-order the\n * list, it HIDES rows from it: a worse failure than the one being fixed, and a\n * silent one.\n *\n * Every collection these cards read has such a writer:\n *\n * * `/api/hosts/resources` stores an ALLOW-LIST of fields and validates none\n * of them for presence, so a create that sends no name produces a document\n * with no `name` at all.\n * * `/api/hosts/import` copies only the keys a bundle actually carries through\n * `IMPORTABLE_FIELDS` — `variables`, `functions`, `workflows` and `actions`\n * are all in it — so a restored document carries a name only if the exported\n * one had it.\n * * `createdAt`/`updatedAt` are no better: the resources route stamps them\n * server-side and an import's `cleanDoc` stamps `updatedAt` only.\n *\n * A document's NAME is not a field and cannot be absent, so ordering on it\n * drops nothing and the walk is TOTAL: every row is reachable by paging, which\n * is the property the old queries lacked. It is not insertion order — the ids\n * are random — so no list built on it may claim to be in one. What it is is\n * stable, complete, and the same on every load.\n *\n * ## Callers must not re-sort the page\n *\n * Re-sorting a window of an id-ordered walk by name is the same lie the old\n * code told: rows would run in one order within a page and another across\n * pages, and the first page would still not be the alphabetical first page. A\n * caller that holds a whole CEILING may sort — see `ceilingedWindow` — because\n * it is sorting the entire collection rather than a slice of it.\n *\n * ## Why a `Query` is accepted and not only a `CollectionReference`\n *\n * A list is often a FILTERED collection — one kind of conversion, one channel\n * — and a caller holding equality predicates could not reach this ordering\n * without rebuilding it, which is how a second, unordered `limit()` comes to\n * be written beside the one this module exists to replace. `query()` composes\n * onto either, and a `CollectionReference` IS a `Query`, so widening the\n * parameter adds no behavior: the walk is still ordered on the document name\n * and is still total over the matching documents.\n *\n * Equality predicates only. A range filter or a second ordering would fight\n * the `orderBy(documentId())` applied here — Firestore requires the range\n * field to be ordered first — and such a query fails loudly at the listener\n * rather than quietly returning the wrong rows.\n */\nexport function collectionPage(\n ref: CollectionReference | Query,\n pageLimit: number,\n): Query {\n return query(ref, orderBy(documentId()), limit(pageLimit))\n}\n\n/**\n * The same ordering, for a list that is BOUNDED rather than paged.\n *\n * Asks for one document more than the ceiling, which is what turns \"there is\n * more than this\" into a fact. Pair it with `ceilingedWindow` to drop the probe\n * row before rendering.\n */\nexport function collectionCeiling(\n ref: CollectionReference | Query,\n ceiling: number,\n): Query {\n return query(ref, orderBy(documentId()), limit(ceiling + 1))\n}\n\n/** A ceilinged read, and whether the ceiling actually bit. */\nexport interface CeilingedWindow<T> {\n /** At most `ceiling` rows — the probe is never among them. */\n rows: T[]\n /** The collection holds MORE than the ceiling. A fact, not an estimate. */\n truncated: boolean\n}\n\n/**\n * A read that is bounded but not paged, and can say when it is short.\n *\n * Some lists cannot be windowed by document and must still be bounded: a\n * hierarchy sliced by row separates a child from its parent, a bundle\n * straddling a page renders twice and partial each time, a precedence list\n * paged by ten cannot swap its tenth row with its eleventh, and a table\n * assembled from two collections cannot dedupe across a page boundary.\n *\n * A bare ceiling is silent, which is the half that matters. Every consequence\n * on those surfaces is computed from what was read, so a collection above the\n * ceiling is drawn as a whole one with nothing to distinguish \"there is no such\n * row\" from \"this card did not read it\".\n *\n * Asking for ONE document more than the ceiling turns that into a fact for the\n * price of a single read. The probe is never handed on: a caller that rendered\n * `ceiling + 1` rows would be describing a window it did not draw.\n *\n * `hasMore` in `usePagedCollection` is the same trick for a paged list, and for\n * the same reason — a comparison against the cap is wrong exactly when the\n * count is an even multiple of it.\n */\nexport function ceilingedWindow<T>(\n read: readonly T[] | undefined,\n ceiling: number,\n): CeilingedWindow<T> {\n const rows = read ?? []\n return { rows: rows.slice(0, ceiling), truncated: rows.length > ceiling }\n}\n"],"names":["documentId","limit","orderBy","query","collectionPage","ref","pageLimit","collectionCeiling","ceiling","ceilingedWindow","read","rows","slice","truncated","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,KAAK,QAGA,qBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoEC,GACD,OAAO,SAASC,eACdC,GAAgC,EAChCC,SAAiB;IAEjB,OAAOH,MAAME,KAAKH,QAAQF,eAAeC,MAAMK;AACjD;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,kBACdF,GAAgC,EAChCG,OAAe;IAEf,OAAOL,MAAME,KAAKH,QAAQF,eAAeC,MAAMO,UAAU;AAC3D;AAUA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,gBACdC,IAA8B,EAC9BF,OAAe;IAEf,MAAMG,OAAOD,eAAAA,OAAQ,EAAE;IACvB,OAAO;QAAEC,MAAMA,KAAKC,KAAK,CAAC,GAAGJ;QAAUK,WAAWF,KAAKG,MAAM,GAAGN;IAAQ;AAC1E"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type QueryConstraint } from 'firebase/firestore';
|
|
18
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
19
|
+
export interface ListFilterConstraintOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The list's own sort field, when the caller owns it. Set it and the
|
|
22
|
+
* translator adds predicates WITHOUT an `orderBy` of its own, refusing any
|
|
23
|
+
* that would need a different one — see the server twin's `fixedOrderBy`.
|
|
24
|
+
*/
|
|
25
|
+
fixedOrderBy?: string;
|
|
26
|
+
/** Sort field for a `contains`, which cannot order by its token array. */
|
|
27
|
+
containsOrderBy?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Constraints for one declared filter, or `null` when this list cannot serve
|
|
31
|
+
* the ask.
|
|
32
|
+
*
|
|
33
|
+
* `null` means "not filtered", and the caller queries unfiltered. That is
|
|
34
|
+
* deliberate and it is the whole point: an operator the console cannot serve
|
|
35
|
+
* must not read as "no such record", which is what an empty page would say.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listFilterConstraints(fields: readonly ListFilterField[], input: ListFilterRequest | null, options?: ListFilterConstraintOptions): QueryConstraint[] | null;
|
|
38
|
+
/** How a list applies several clauses at once — see {@link listFilterPlan}. */
|
|
39
|
+
export interface ListFilterPlan {
|
|
40
|
+
/** The one clause the query carries, or none. */
|
|
41
|
+
served: ListFilterRequest | null;
|
|
42
|
+
/** Its constraints, or `null` when nothing was servable. */
|
|
43
|
+
constraints: QueryConstraint[] | null;
|
|
44
|
+
/** Every other clause, matched over the rows the query returns. */
|
|
45
|
+
window: ListFilterRequest[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Several clauses, split into the ONE the query serves and the rest the
|
|
49
|
+
* list matches in memory (AGL-2617).
|
|
50
|
+
*
|
|
51
|
+
* A saved view carries a list of clauses and Firestore composes predicates
|
|
52
|
+
* only through indexes nobody built for every pair of columns — and takes
|
|
53
|
+
* one array clause per query, which the scope predicate already is. So the
|
|
54
|
+
* first clause the translator can serve goes to the query, where it reaches
|
|
55
|
+
* the whole collection, and every other clause narrows the bounded window
|
|
56
|
+
* that query returns. The list says which is which in its caption: a clause
|
|
57
|
+
* that narrows only the window must not read as one that searched
|
|
58
|
+
* everything.
|
|
59
|
+
*
|
|
60
|
+
* First SERVABLE, not first listed: a reader who adds an owner clause (a
|
|
61
|
+
* facet field no query reaches) and then a name prefix should have the
|
|
62
|
+
* name searched, not the owner "served" as nothing.
|
|
63
|
+
*/
|
|
64
|
+
export declare function listFilterPlan(fields: readonly ListFilterField[], clauses: readonly ListFilterRequest[], options?: ListFilterConstraintOptions): ListFilterPlan;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { documentId, endAt, orderBy, startAt, Timestamp, where } from "firebase/firestore";
|
|
17
|
+
/**
|
|
18
|
+
* The same filter contract, for a list that queries Firestore from the BROWSER
|
|
19
|
+
* (AGL-2501).
|
|
20
|
+
*
|
|
21
|
+
* The console's paged cards read their collections directly through
|
|
22
|
+
* `usePagedCollection` rather than through a route, so the server translator —
|
|
23
|
+
* which speaks firebase-admin — cannot serve them. This is its twin in the web
|
|
24
|
+
* SDK, reading the same `ListFilterField` declaration so a list cannot offer an
|
|
25
|
+
* operator its query will not answer.
|
|
26
|
+
*
|
|
27
|
+
* The reason it must exist at all is that these cards filtered the rows they
|
|
28
|
+
* had. A paged card holds ten; typing a name that is on page four answers "no
|
|
29
|
+
* such member", which reads as the record not existing rather than as the
|
|
30
|
+
* search not reaching it.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ A predicate here runs under SECURITY RULES, unlike the server twin. A
|
|
33
|
+
* field a rule does not permit reading is not a field this can filter on, and
|
|
34
|
+
* the failure arrives as a permission error rather than as an empty page.
|
|
35
|
+
*/ /** Firestore caps `in` at 30 values. */ const IN_LIMIT = 30;
|
|
36
|
+
/** A very high private-use codepoint — see the server twin for why. */ const HIGH = '\uf8ff';
|
|
37
|
+
const csv = (raw)=>raw.split(',').map((entry)=>entry.trim()).filter(Boolean).slice(0, IN_LIMIT);
|
|
38
|
+
const key = (value)=>value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
39
|
+
/** Reverse by CODEPOINT — `split('')` tears surrogate pairs in half. */ const reversed = (value)=>[
|
|
40
|
+
...key(value)
|
|
41
|
+
].reverse().join('');
|
|
42
|
+
const toDay = (value)=>{
|
|
43
|
+
const parsed = new Date(value);
|
|
44
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
45
|
+
const start = new Date(parsed);
|
|
46
|
+
start.setHours(0, 0, 0, 0);
|
|
47
|
+
const end = new Date(start);
|
|
48
|
+
end.setDate(end.getDate() + 1);
|
|
49
|
+
return {
|
|
50
|
+
start,
|
|
51
|
+
end
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Constraints for one declared filter, or `null` when this list cannot serve
|
|
56
|
+
* the ask.
|
|
57
|
+
*
|
|
58
|
+
* `null` means "not filtered", and the caller queries unfiltered. That is
|
|
59
|
+
* deliberate and it is the whole point: an operator the console cannot serve
|
|
60
|
+
* must not read as "no such record", which is what an empty page would say.
|
|
61
|
+
*/ export function listFilterConstraints(fields, input, options = {}) {
|
|
62
|
+
var _input_value;
|
|
63
|
+
if (!input) return null;
|
|
64
|
+
const field = fields.find((entry)=>entry.column === input.field);
|
|
65
|
+
if (!field) return null;
|
|
66
|
+
// A window-only field is matched over the loaded rows — see the flag.
|
|
67
|
+
if (field.windowOnly) return null;
|
|
68
|
+
const raw = ((_input_value = input.value) != null ? _input_value : '').trim();
|
|
69
|
+
const op = input.op;
|
|
70
|
+
const pinned = options.fixedOrderBy;
|
|
71
|
+
const sorted = (by)=>pinned ? [] : [
|
|
72
|
+
orderBy(by)
|
|
73
|
+
];
|
|
74
|
+
const rangeAllowed = (path)=>!pinned || pinned === path;
|
|
75
|
+
if (op === 'isEmpty') {
|
|
76
|
+
// Only where writers store `null`; Firestore cannot find documents that
|
|
77
|
+
// simply LACK a field. See `presence` on the shared declaration.
|
|
78
|
+
if (field.presence !== 'nullable') return null;
|
|
79
|
+
return [
|
|
80
|
+
where(field.path, '==', null),
|
|
81
|
+
...sorted(documentId())
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
if (op === 'isNotEmpty') {
|
|
85
|
+
// `!=` also requires the field to EXIST, which is the meaning wanted.
|
|
86
|
+
if (field.presence === 'always') return null;
|
|
87
|
+
if (!rangeAllowed(field.path)) return null;
|
|
88
|
+
return [
|
|
89
|
+
where(field.path, '!=', null),
|
|
90
|
+
...sorted(field.path)
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
if (field.kind === 'boolean') {
|
|
94
|
+
if (raw !== 'true' && raw !== 'false') return null;
|
|
95
|
+
return [
|
|
96
|
+
where(field.path, '==', raw === 'true'),
|
|
97
|
+
...sorted(documentId())
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
if (!raw) return null;
|
|
101
|
+
const prefixRange = (path, prefix)=>prefix && rangeAllowed(path) ? [
|
|
102
|
+
orderBy(path),
|
|
103
|
+
startAt(prefix),
|
|
104
|
+
endAt(`${prefix}${HIGH}`)
|
|
105
|
+
] : null;
|
|
106
|
+
if (field.kind === 'text') {
|
|
107
|
+
if (op === 'contains' && field.tokensPath) {
|
|
108
|
+
var _ref, _ref1, _field_containsOrderBy;
|
|
109
|
+
// An id array is matched as typed — see `verbatimTokens`.
|
|
110
|
+
const token = field.verbatimTokens ? raw : key(raw).split(' ')[0];
|
|
111
|
+
if (!token) return null;
|
|
112
|
+
const sortBy = (_ref = (_ref1 = (_field_containsOrderBy = field.containsOrderBy) != null ? _field_containsOrderBy : options.containsOrderBy) != null ? _ref1 : field.lowerPath) != null ? _ref : field.path;
|
|
113
|
+
return [
|
|
114
|
+
where(field.tokensPath, 'array-contains', token),
|
|
115
|
+
...pinned ? [] : [
|
|
116
|
+
orderBy(sortBy)
|
|
117
|
+
]
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
if (op === 'equals' && field.lowerPath) {
|
|
121
|
+
return [
|
|
122
|
+
where(field.lowerPath, '==', key(raw)),
|
|
123
|
+
...sorted(documentId())
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
if (op === 'startsWith' && field.lowerPath) {
|
|
127
|
+
return prefixRange(field.lowerPath, key(raw));
|
|
128
|
+
}
|
|
129
|
+
if (op === 'endsWith' && field.reversedPath) {
|
|
130
|
+
return prefixRange(field.reversedPath, reversed(raw));
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (field.kind === 'id') {
|
|
135
|
+
if (op === 'equals') {
|
|
136
|
+
return [
|
|
137
|
+
where(documentId(), '==', raw),
|
|
138
|
+
...sorted(documentId())
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
if (op === 'startsWith') {
|
|
142
|
+
if (pinned) return null;
|
|
143
|
+
return [
|
|
144
|
+
orderBy(documentId()),
|
|
145
|
+
startAt(raw),
|
|
146
|
+
endAt(`${raw}${HIGH}`)
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
if (op === 'isAnyOf') {
|
|
150
|
+
const values = csv(raw);
|
|
151
|
+
return values.length ? [
|
|
152
|
+
where(documentId(), 'in', values),
|
|
153
|
+
...sorted(documentId())
|
|
154
|
+
] : null;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (field.keysOf) {
|
|
159
|
+
// One key of a presence map, as the equality on its own path. A key is
|
|
160
|
+
// an identifier the writer chose; anything else could not be a path.
|
|
161
|
+
if (op !== 'equals' || !/^[A-Za-z0-9_]+$/.test(raw)) return null;
|
|
162
|
+
return [
|
|
163
|
+
where(`${field.path}.${raw}`, '==', true),
|
|
164
|
+
...sorted(documentId())
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
if (field.kind === 'exact') {
|
|
168
|
+
if (op === 'equals') {
|
|
169
|
+
return [
|
|
170
|
+
where(field.path, '==', raw),
|
|
171
|
+
...sorted(documentId())
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
if (op === 'isAnyOf') {
|
|
175
|
+
const values = csv(raw);
|
|
176
|
+
return values.length ? [
|
|
177
|
+
where(field.path, 'in', values),
|
|
178
|
+
...sorted(documentId())
|
|
179
|
+
] : null;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (field.kind === 'number') {
|
|
184
|
+
const value = Number(raw);
|
|
185
|
+
if (!Number.isFinite(value)) return null;
|
|
186
|
+
if (op === '=') {
|
|
187
|
+
return [
|
|
188
|
+
where(field.path, '==', value),
|
|
189
|
+
...sorted(documentId())
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
const comparison = {
|
|
193
|
+
'!=': '!=',
|
|
194
|
+
'>': '>',
|
|
195
|
+
'>=': '>=',
|
|
196
|
+
'<': '<',
|
|
197
|
+
'<=': '<='
|
|
198
|
+
};
|
|
199
|
+
const operator = comparison[op];
|
|
200
|
+
if (!operator || !rangeAllowed(field.path)) return null;
|
|
201
|
+
return [
|
|
202
|
+
where(field.path, operator, value),
|
|
203
|
+
...sorted(field.path)
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
if (field.kind === 'date') {
|
|
207
|
+
const day = toDay(raw);
|
|
208
|
+
if (!day || !rangeAllowed(field.path)) return null;
|
|
209
|
+
const stamp = (date)=>Timestamp.fromDate(date);
|
|
210
|
+
/*
|
|
211
|
+
* `is` is a DAY, not an instant. A stored timestamp carries a time, so
|
|
212
|
+
* equality against midnight matches nothing — a date column filtered by
|
|
213
|
+
* `is` would answer "none" for every row, every time.
|
|
214
|
+
*/ if (op === 'is') {
|
|
215
|
+
return [
|
|
216
|
+
...pinned ? [] : [
|
|
217
|
+
orderBy(field.path)
|
|
218
|
+
],
|
|
219
|
+
startAt(stamp(day.start)),
|
|
220
|
+
endAt(stamp(day.end))
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
const bound = {
|
|
224
|
+
after: [
|
|
225
|
+
'>=',
|
|
226
|
+
day.end
|
|
227
|
+
],
|
|
228
|
+
onOrAfter: [
|
|
229
|
+
'>=',
|
|
230
|
+
day.start
|
|
231
|
+
],
|
|
232
|
+
before: [
|
|
233
|
+
'<',
|
|
234
|
+
day.start
|
|
235
|
+
],
|
|
236
|
+
onOrBefore: [
|
|
237
|
+
'<',
|
|
238
|
+
day.end
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
const found = bound[op];
|
|
242
|
+
if (!found) return null;
|
|
243
|
+
return [
|
|
244
|
+
where(field.path, found[0], stamp(found[1])),
|
|
245
|
+
...sorted(field.path)
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Several clauses, split into the ONE the query serves and the rest the
|
|
252
|
+
* list matches in memory (AGL-2617).
|
|
253
|
+
*
|
|
254
|
+
* A saved view carries a list of clauses and Firestore composes predicates
|
|
255
|
+
* only through indexes nobody built for every pair of columns — and takes
|
|
256
|
+
* one array clause per query, which the scope predicate already is. So the
|
|
257
|
+
* first clause the translator can serve goes to the query, where it reaches
|
|
258
|
+
* the whole collection, and every other clause narrows the bounded window
|
|
259
|
+
* that query returns. The list says which is which in its caption: a clause
|
|
260
|
+
* that narrows only the window must not read as one that searched
|
|
261
|
+
* everything.
|
|
262
|
+
*
|
|
263
|
+
* First SERVABLE, not first listed: a reader who adds an owner clause (a
|
|
264
|
+
* facet field no query reaches) and then a name prefix should have the
|
|
265
|
+
* name searched, not the owner "served" as nothing.
|
|
266
|
+
*/ export function listFilterPlan(fields, clauses, options = {}) {
|
|
267
|
+
let served = null;
|
|
268
|
+
let constraints = null;
|
|
269
|
+
const window = [];
|
|
270
|
+
for (const clause of clauses){
|
|
271
|
+
if (!served) {
|
|
272
|
+
const built = listFilterConstraints(fields, clause, options);
|
|
273
|
+
if (built) {
|
|
274
|
+
served = clause;
|
|
275
|
+
constraints = built;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
window.push(clause);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
served,
|
|
283
|
+
constraints,
|
|
284
|
+
window
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//# sourceMappingURL=list-filter-constraints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/list-filter-constraints.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 {\n documentId,\n endAt,\n orderBy,\n startAt,\n Timestamp,\n where,\n type QueryConstraint,\n} from 'firebase/firestore'\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\n\n/**\n * The same filter contract, for a list that queries Firestore from the BROWSER\n * (AGL-2501).\n *\n * The console's paged cards read their collections directly through\n * `usePagedCollection` rather than through a route, so the server translator —\n * which speaks firebase-admin — cannot serve them. This is its twin in the web\n * SDK, reading the same `ListFilterField` declaration so a list cannot offer an\n * operator its query will not answer.\n *\n * The reason it must exist at all is that these cards filtered the rows they\n * had. A paged card holds ten; typing a name that is on page four answers \"no\n * such member\", which reads as the record not existing rather than as the\n * search not reaching it.\n *\n * ⚠️ A predicate here runs under SECURITY RULES, unlike the server twin. A\n * field a rule does not permit reading is not a field this can filter on, and\n * the failure arrives as a permission error rather than as an empty page.\n */\n\n/** Firestore caps `in` at 30 values. */\nconst IN_LIMIT = 30\n\n/** A very high private-use codepoint — see the server twin for why. */\nconst HIGH = '\\uf8ff'\n\nconst csv = (raw: string): string[] =>\n raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .slice(0, IN_LIMIT)\n\nconst key = (value: string): string =>\n value.trim().replace(/\\s+/g, ' ').toLowerCase()\n\n/** Reverse by CODEPOINT — `split('')` tears surrogate pairs in half. */\nconst reversed = (value: string): string => [...key(value)].reverse().join('')\n\nconst toDay = (value: string): { start: Date; end: Date } | null => {\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return null\n const start = new Date(parsed)\n start.setHours(0, 0, 0, 0)\n const end = new Date(start)\n end.setDate(end.getDate() + 1)\n return { start, end }\n}\n\nexport interface ListFilterConstraintOptions {\n /**\n * The list's own sort field, when the caller owns it. Set it and the\n * translator adds predicates WITHOUT an `orderBy` of its own, refusing any\n * that would need a different one — see the server twin's `fixedOrderBy`.\n */\n fixedOrderBy?: string\n /** Sort field for a `contains`, which cannot order by its token array. */\n containsOrderBy?: string\n}\n\n/**\n * Constraints for one declared filter, or `null` when this list cannot serve\n * the ask.\n *\n * `null` means \"not filtered\", and the caller queries unfiltered. That is\n * deliberate and it is the whole point: an operator the console cannot serve\n * must not read as \"no such record\", which is what an empty page would say.\n */\nexport function listFilterConstraints(\n fields: readonly ListFilterField[],\n input: ListFilterRequest | null,\n options: ListFilterConstraintOptions = {},\n): QueryConstraint[] | null {\n if (!input) return null\n const field = fields.find((entry) => entry.column === input.field)\n if (!field) return null\n // A window-only field is matched over the loaded rows — see the flag.\n if (field.windowOnly) return null\n const raw = (input.value ?? '').trim()\n const op = input.op\n const pinned = options.fixedOrderBy\n const sorted = (by: string | ReturnType<typeof documentId>) =>\n pinned ? [] : [orderBy(by as never)]\n const rangeAllowed = (path: string) => !pinned || pinned === path\n\n if (op === 'isEmpty') {\n // Only where writers store `null`; Firestore cannot find documents that\n // simply LACK a field. See `presence` on the shared declaration.\n if (field.presence !== 'nullable') return null\n return [where(field.path, '==', null), ...sorted(documentId())]\n }\n if (op === 'isNotEmpty') {\n // `!=` also requires the field to EXIST, which is the meaning wanted.\n if (field.presence === 'always') return null\n if (!rangeAllowed(field.path)) return null\n return [where(field.path, '!=', null), ...sorted(field.path)]\n }\n\n if (field.kind === 'boolean') {\n if (raw !== 'true' && raw !== 'false') return null\n return [where(field.path, '==', raw === 'true'), ...sorted(documentId())]\n }\n\n if (!raw) return null\n\n const prefixRange = (path: string, prefix: string) =>\n prefix && rangeAllowed(path)\n ? [orderBy(path), startAt(prefix), endAt(`${prefix}${HIGH}`)]\n : null\n\n if (field.kind === 'text') {\n if (op === 'contains' && field.tokensPath) {\n // An id array is matched as typed — see `verbatimTokens`.\n const token = field.verbatimTokens ? raw : key(raw).split(' ')[0]\n if (!token) return null\n const sortBy = field.containsOrderBy ?? options.containsOrderBy ?? field.lowerPath ?? field.path\n return [\n where(field.tokensPath, 'array-contains', token),\n ...(pinned ? [] : [orderBy(sortBy)]),\n ]\n }\n if (op === 'equals' && field.lowerPath) {\n return [where(field.lowerPath, '==', key(raw)), ...sorted(documentId())]\n }\n if (op === 'startsWith' && field.lowerPath) {\n return prefixRange(field.lowerPath, key(raw))\n }\n if (op === 'endsWith' && field.reversedPath) {\n return prefixRange(field.reversedPath, reversed(raw))\n }\n return null\n }\n\n if (field.kind === 'id') {\n if (op === 'equals') {\n return [where(documentId(), '==', raw), ...sorted(documentId())]\n }\n if (op === 'startsWith') {\n if (pinned) return null\n return [orderBy(documentId()), startAt(raw), endAt(`${raw}${HIGH}`)]\n }\n if (op === 'isAnyOf') {\n const values = csv(raw)\n return values.length\n ? [where(documentId(), 'in', values), ...sorted(documentId())]\n : null\n }\n return null\n }\n\n if (field.keysOf) {\n // One key of a presence map, as the equality on its own path. A key is\n // an identifier the writer chose; anything else could not be a path.\n if (op !== 'equals' || !/^[A-Za-z0-9_]+$/.test(raw)) return null\n return [where(`${field.path}.${raw}`, '==', true), ...sorted(documentId())]\n }\n\n if (field.kind === 'exact') {\n if (op === 'equals') {\n return [where(field.path, '==', raw), ...sorted(documentId())]\n }\n if (op === 'isAnyOf') {\n const values = csv(raw)\n return values.length\n ? [where(field.path, 'in', values), ...sorted(documentId())]\n : null\n }\n return null\n }\n\n if (field.kind === 'number') {\n const value = Number(raw)\n if (!Number.isFinite(value)) return null\n if (op === '=') {\n return [where(field.path, '==', value), ...sorted(documentId())]\n }\n const comparison: Record<string, '!=' | '>' | '>=' | '<' | '<='> = {\n '!=': '!=',\n '>': '>',\n '>=': '>=',\n '<': '<',\n '<=': '<=',\n }\n const operator = comparison[op]\n if (!operator || !rangeAllowed(field.path)) return null\n return [where(field.path, operator, value), ...sorted(field.path)]\n }\n\n if (field.kind === 'date') {\n const day = toDay(raw)\n if (!day || !rangeAllowed(field.path)) return null\n const stamp = (date: Date) => Timestamp.fromDate(date)\n /*\n * `is` is a DAY, not an instant. A stored timestamp carries a time, so\n * equality against midnight matches nothing — a date column filtered by\n * `is` would answer \"none\" for every row, every time.\n */\n if (op === 'is') {\n return [\n ...(pinned ? [] : [orderBy(field.path)]),\n startAt(stamp(day.start)),\n endAt(stamp(day.end)),\n ]\n }\n const bound: Record<string, ['>=' | '<', Date]> = {\n after: ['>=', day.end],\n onOrAfter: ['>=', day.start],\n before: ['<', day.start],\n onOrBefore: ['<', day.end],\n }\n const found = bound[op]\n if (!found) return null\n return [\n where(field.path, found[0], stamp(found[1])),\n ...sorted(field.path),\n ]\n }\n\n return null\n}\n\n/** How a list applies several clauses at once — see {@link listFilterPlan}. */\nexport interface ListFilterPlan {\n /** The one clause the query carries, or none. */\n served: ListFilterRequest | null\n /** Its constraints, or `null` when nothing was servable. */\n constraints: QueryConstraint[] | null\n /** Every other clause, matched over the rows the query returns. */\n window: ListFilterRequest[]\n}\n\n/**\n * Several clauses, split into the ONE the query serves and the rest the\n * list matches in memory (AGL-2617).\n *\n * A saved view carries a list of clauses and Firestore composes predicates\n * only through indexes nobody built for every pair of columns — and takes\n * one array clause per query, which the scope predicate already is. So the\n * first clause the translator can serve goes to the query, where it reaches\n * the whole collection, and every other clause narrows the bounded window\n * that query returns. The list says which is which in its caption: a clause\n * that narrows only the window must not read as one that searched\n * everything.\n *\n * First SERVABLE, not first listed: a reader who adds an owner clause (a\n * facet field no query reaches) and then a name prefix should have the\n * name searched, not the owner \"served\" as nothing.\n */\nexport function listFilterPlan(\n fields: readonly ListFilterField[],\n clauses: readonly ListFilterRequest[],\n options: ListFilterConstraintOptions = {},\n): ListFilterPlan {\n let served: ListFilterRequest | null = null\n let constraints: QueryConstraint[] | null = null\n const window: ListFilterRequest[] = []\n for (const clause of clauses) {\n if (!served) {\n const built = listFilterConstraints(fields, clause, options)\n if (built) {\n served = clause\n constraints = built\n continue\n }\n }\n window.push(clause)\n }\n return { served, constraints, window }\n}\n"],"names":["documentId","endAt","orderBy","startAt","Timestamp","where","IN_LIMIT","HIGH","csv","raw","split","map","entry","trim","filter","Boolean","slice","key","value","replace","toLowerCase","reversed","reverse","join","toDay","parsed","Date","Number","isNaN","getTime","start","setHours","end","setDate","getDate","listFilterConstraints","fields","input","options","field","find","column","windowOnly","op","pinned","fixedOrderBy","sorted","by","rangeAllowed","path","presence","kind","prefixRange","prefix","tokensPath","token","verbatimTokens","sortBy","containsOrderBy","lowerPath","reversedPath","values","length","keysOf","test","isFinite","comparison","operator","day","stamp","date","fromDate","bound","after","onOrAfter","before","onOrBefore","found","listFilterPlan","clauses","served","constraints","window","clause","built","push"],"mappings":"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"}
|