@aglyn/tenant-feature-instance 1.0.0-beta.195 → 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-feature-instance",
|
|
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": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.196",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.196",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.196",
|
|
31
|
+
"@aglyn/shared-util-http": "1.0.0-beta.196",
|
|
32
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.196",
|
|
33
33
|
"@swc/helpers": "0.5.23",
|
|
34
34
|
"lodash-es": "^4.18.1"
|
|
35
35
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type QueryConstraint } from 'firebase/firestore';
|
|
18
|
-
import type
|
|
18
|
+
import { type ListFilterField, type ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
19
19
|
export interface ListFilterConstraintOptions {
|
|
20
20
|
/**
|
|
21
21
|
* The list's own sort field, when the caller owns it. Set it and the
|
|
@@ -25,6 +25,21 @@ export interface ListFilterConstraintOptions {
|
|
|
25
25
|
fixedOrderBy?: string;
|
|
26
26
|
/** Sort field for a `contains`, which cannot order by its token array. */
|
|
27
27
|
containsOrderBy?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The query already carries an `array-contains-any` over `values` scope
|
|
30
|
+
* tokens (AGL-3321).
|
|
31
|
+
*
|
|
32
|
+
* Firestore takes ONE array clause per query, so a `contains` on a token
|
|
33
|
+
* array cannot stand beside it and is left to the window. `exempt` names
|
|
34
|
+
* the fields whose caller drops the scope clause for them instead. And an
|
|
35
|
+
* `in` multiplies with the scope's values into disjunctions, which
|
|
36
|
+
* Firestore caps at thirty, so an `isAnyOf` longer than the budget left is
|
|
37
|
+
* left to the window too rather than refused at query time.
|
|
38
|
+
*/
|
|
39
|
+
arrayScope?: {
|
|
40
|
+
values: number;
|
|
41
|
+
exempt?: readonly string[];
|
|
42
|
+
};
|
|
28
43
|
}
|
|
29
44
|
/**
|
|
30
45
|
* Constraints for one declared filter, or `null` when this list cannot serve
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { documentId, endAt, orderBy, startAt, Timestamp, where } from "firebase/firestore";
|
|
17
|
+
import { listFilterDay } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
17
18
|
/**
|
|
18
19
|
* The same filter contract, for a list that queries Firestore from the BROWSER
|
|
19
20
|
* (AGL-2501).
|
|
@@ -34,13 +35,16 @@
|
|
|
34
35
|
* the failure arrives as a permission error rather than as an empty page.
|
|
35
36
|
*/ /** Firestore caps `in` at 30 values. */ const IN_LIMIT = 30;
|
|
36
37
|
/** 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)
|
|
38
|
+
const csv = (raw)=>raw.split(',').map((entry)=>entry.trim()).filter(Boolean);
|
|
38
39
|
const key = (value)=>value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
39
40
|
/** Reverse by CODEPOINT — `split('')` tears surrogate pairs in half. */ const reversed = (value)=>[
|
|
40
41
|
...key(value)
|
|
41
42
|
].reverse().join('');
|
|
42
43
|
const toDay = (value)=>{
|
|
43
|
-
|
|
44
|
+
// The calendar day the clause names, in the reader's zone — see
|
|
45
|
+
// `listFilterDay`; `new Date('YYYY-MM-DD')` is UTC midnight, the evening
|
|
46
|
+
// before anywhere west of UTC.
|
|
47
|
+
const parsed = listFilterDay(value);
|
|
44
48
|
if (Number.isNaN(parsed.getTime())) return null;
|
|
45
49
|
const start = new Date(parsed);
|
|
46
50
|
start.setHours(0, 0, 0, 0);
|
|
@@ -51,6 +55,7 @@ const toDay = (value)=>{
|
|
|
51
55
|
end
|
|
52
56
|
};
|
|
53
57
|
};
|
|
58
|
+
/** The disjunctions Firestore allows one query. */ const DISJUNCTION_LIMIT = 30;
|
|
54
59
|
/**
|
|
55
60
|
* Constraints for one declared filter, or `null` when this list cannot serve
|
|
56
61
|
* the ask.
|
|
@@ -59,7 +64,8 @@ const toDay = (value)=>{
|
|
|
59
64
|
* deliberate and it is the whole point: an operator the console cannot serve
|
|
60
65
|
* must not read as "no such record", which is what an empty page would say.
|
|
61
66
|
*/ export function listFilterConstraints(fields, input, options = {}) {
|
|
62
|
-
var _input_value;
|
|
67
|
+
var _input_value, _ref;
|
|
68
|
+
var _scope_exempt;
|
|
63
69
|
if (!input) return null;
|
|
64
70
|
const field = fields.find((entry)=>entry.column === input.field);
|
|
65
71
|
if (!field) return null;
|
|
@@ -68,6 +74,13 @@ const toDay = (value)=>{
|
|
|
68
74
|
const raw = ((_input_value = input.value) != null ? _input_value : '').trim();
|
|
69
75
|
const op = input.op;
|
|
70
76
|
const pinned = options.fixedOrderBy;
|
|
77
|
+
const scope = options.arrayScope;
|
|
78
|
+
const scoped = Boolean(scope) && !(scope == null ? void 0 : (_scope_exempt = scope.exempt) == null ? void 0 : _scope_exempt.includes(field.column));
|
|
79
|
+
/** How many `in` values fit beside the scope clause's own. */ const inBudget = scoped ? Math.floor(DISJUNCTION_LIMIT / Math.max(1, (_ref = scope == null ? void 0 : scope.values) != null ? _ref : 1)) : IN_LIMIT;
|
|
80
|
+
const inValues = (raw)=>{
|
|
81
|
+
const values = csv(raw);
|
|
82
|
+
return values.length && values.length <= inBudget ? values : null;
|
|
83
|
+
};
|
|
71
84
|
const sorted = (by)=>pinned ? [] : [
|
|
72
85
|
orderBy(by)
|
|
73
86
|
];
|
|
@@ -105,15 +118,17 @@ const toDay = (value)=>{
|
|
|
105
118
|
] : null;
|
|
106
119
|
if (field.kind === 'text') {
|
|
107
120
|
if (op === 'contains' && field.tokensPath) {
|
|
108
|
-
var
|
|
121
|
+
var _ref1, _ref2, _field_containsOrderBy, _field_containsOrderDirection;
|
|
122
|
+
// One array clause per query, and the scope clause is one.
|
|
123
|
+
if (scoped) return null;
|
|
109
124
|
// An id array is matched as typed — see `verbatimTokens`.
|
|
110
125
|
const token = field.verbatimTokens ? raw : key(raw).split(' ')[0];
|
|
111
126
|
if (!token) return null;
|
|
112
|
-
const sortBy = (
|
|
127
|
+
const sortBy = (_ref1 = (_ref2 = (_field_containsOrderBy = field.containsOrderBy) != null ? _field_containsOrderBy : options.containsOrderBy) != null ? _ref2 : field.lowerPath) != null ? _ref1 : field.path;
|
|
113
128
|
return [
|
|
114
129
|
where(field.tokensPath, 'array-contains', token),
|
|
115
130
|
...pinned ? [] : [
|
|
116
|
-
orderBy(sortBy)
|
|
131
|
+
orderBy(sortBy, (_field_containsOrderDirection = field.containsOrderDirection) != null ? _field_containsOrderDirection : 'asc')
|
|
117
132
|
]
|
|
118
133
|
];
|
|
119
134
|
}
|
|
@@ -147,8 +162,8 @@ const toDay = (value)=>{
|
|
|
147
162
|
];
|
|
148
163
|
}
|
|
149
164
|
if (op === 'isAnyOf') {
|
|
150
|
-
const values =
|
|
151
|
-
return values
|
|
165
|
+
const values = inValues(raw);
|
|
166
|
+
return values ? [
|
|
152
167
|
where(documentId(), 'in', values),
|
|
153
168
|
...sorted(documentId())
|
|
154
169
|
] : null;
|
|
@@ -172,8 +187,8 @@ const toDay = (value)=>{
|
|
|
172
187
|
];
|
|
173
188
|
}
|
|
174
189
|
if (op === 'isAnyOf') {
|
|
175
|
-
const values =
|
|
176
|
-
return values
|
|
190
|
+
const values = inValues(raw);
|
|
191
|
+
return values ? [
|
|
177
192
|
where(field.path, 'in', values),
|
|
178
193
|
...sorted(documentId())
|
|
179
194
|
] : null;
|
|
@@ -212,12 +227,13 @@ const toDay = (value)=>{
|
|
|
212
227
|
* equality against midnight matches nothing — a date column filtered by
|
|
213
228
|
* `is` would answer "none" for every row, every time.
|
|
214
229
|
*/ if (op === 'is') {
|
|
230
|
+
// Two bounds as `where`s, not a cursor: a cursor must follow the
|
|
231
|
+
// query's own `orderBy`, and a list that owns its sort (and sorts it
|
|
232
|
+
// descending) would have the day read backwards (AGL-3321).
|
|
215
233
|
return [
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
startAt(stamp(day.start)),
|
|
220
|
-
endAt(stamp(day.end))
|
|
234
|
+
where(field.path, '>=', stamp(day.start)),
|
|
235
|
+
where(field.path, '<', stamp(day.end)),
|
|
236
|
+
...sorted(field.path)
|
|
221
237
|
];
|
|
222
238
|
}
|
|
223
239
|
const bound = {
|
|
@@ -1 +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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"}
|
|
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 {\n type ListFilterField,\n type ListFilterRequest,\n listFilterDay,\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\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 // The calendar day the clause names, in the reader's zone — see\n // `listFilterDay`; `new Date('YYYY-MM-DD')` is UTC midnight, the evening\n // before anywhere west of UTC.\n const parsed = listFilterDay(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 * The query already carries an `array-contains-any` over `values` scope\n * tokens (AGL-3321).\n *\n * Firestore takes ONE array clause per query, so a `contains` on a token\n * array cannot stand beside it and is left to the window. `exempt` names\n * the fields whose caller drops the scope clause for them instead. And an\n * `in` multiplies with the scope's values into disjunctions, which\n * Firestore caps at thirty, so an `isAnyOf` longer than the budget left is\n * left to the window too rather than refused at query time.\n */\n arrayScope?: { values: number; exempt?: readonly string[] }\n}\n\n/** The disjunctions Firestore allows one query. */\nconst DISJUNCTION_LIMIT = 30\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 scope = options.arrayScope\n const scoped = Boolean(scope) && !scope?.exempt?.includes(field.column)\n /** How many `in` values fit beside the scope clause's own. */\n const inBudget = scoped\n ? Math.floor(DISJUNCTION_LIMIT / Math.max(1, scope?.values ?? 1))\n : IN_LIMIT\n const inValues = (raw: string): string[] | null => {\n const values = csv(raw)\n return values.length && values.length <= inBudget ? values : null\n }\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 // One array clause per query, and the scope clause is one.\n if (scoped) return null\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, field.containsOrderDirection ?? 'asc')]),\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 = inValues(raw)\n return values ? [where(documentId(), 'in', values), ...sorted(documentId())] : 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 = inValues(raw)\n return values ? [where(field.path, 'in', values), ...sorted(documentId())] : 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 // Two bounds as `where`s, not a cursor: a cursor must follow the\n // query's own `orderBy`, and a list that owns its sort (and sorts it\n // descending) would have the day read backwards (AGL-3321).\n return [\n where(field.path, '>=', stamp(day.start)),\n where(field.path, '<', stamp(day.end)),\n ...sorted(field.path),\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","listFilterDay","IN_LIMIT","HIGH","csv","raw","split","map","entry","trim","filter","Boolean","key","value","replace","toLowerCase","reversed","reverse","join","toDay","parsed","Number","isNaN","getTime","start","Date","setHours","end","setDate","getDate","DISJUNCTION_LIMIT","listFilterConstraints","fields","input","options","scope","field","find","column","windowOnly","op","pinned","fixedOrderBy","arrayScope","scoped","exempt","includes","inBudget","Math","floor","max","values","inValues","length","sorted","by","rangeAllowed","path","presence","kind","prefixRange","prefix","tokensPath","token","verbatimTokens","sortBy","containsOrderBy","lowerPath","containsOrderDirection","reversedPath","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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"}
|