@rebasepro/common 0.23.0 → 0.24.1-canary.g56af4a4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/collections/CollectionRegistry.d.ts +72 -3
  2. package/dist/collections/CollectionRegistry.es.js +404 -0
  3. package/dist/collections/CollectionRegistry.es.js.map +1 -0
  4. package/dist/collections/auth-secrets.es.js +47 -0
  5. package/dist/collections/auth-secrets.es.js.map +1 -0
  6. package/dist/collections/default-collections.es.js +120 -0
  7. package/dist/collections/default-collections.es.js.map +1 -0
  8. package/dist/collections/engine-support.es.js +35 -0
  9. package/dist/collections/engine-support.es.js.map +1 -0
  10. package/dist/collections/field-access.d.ts +13 -0
  11. package/dist/collections/field-access.es.js +142 -0
  12. package/dist/collections/field-access.es.js.map +1 -0
  13. package/dist/collections/index.d.ts +1 -0
  14. package/dist/collections/soft-delete.es.js +34 -0
  15. package/dist/collections/soft-delete.es.js.map +1 -0
  16. package/dist/collections/unknown-field.d.ts +65 -0
  17. package/dist/collections/unknown-field.es.js +135 -0
  18. package/dist/collections/unknown-field.es.js.map +1 -0
  19. package/dist/data/buildRebaseData.d.ts +27 -2
  20. package/dist/data/buildRebaseData.es.js +1026 -0
  21. package/dist/data/buildRebaseData.es.js.map +1 -0
  22. package/dist/data/buildRoutedRebaseData.d.ts +6 -1
  23. package/dist/data/buildRoutedRebaseData.es.js +47 -0
  24. package/dist/data/buildRoutedRebaseData.es.js.map +1 -0
  25. package/dist/data/collection-accessor.d.ts +54 -0
  26. package/dist/data/collection-accessor.es.js +101 -0
  27. package/dist/data/collection-accessor.es.js.map +1 -0
  28. package/dist/data/cursor.d.ts +1 -1
  29. package/dist/data/cursor.es.js +166 -0
  30. package/dist/data/cursor.es.js.map +1 -0
  31. package/dist/data/filter-conditions.es.js +24 -0
  32. package/dist/data/filter-conditions.es.js.map +1 -0
  33. package/dist/data/filter-dialect.d.ts +3 -3
  34. package/dist/data/filter-dialect.es.js +786 -0
  35. package/dist/data/filter-dialect.es.js.map +1 -0
  36. package/dist/data/include-spec.d.ts +2 -0
  37. package/dist/data/include-spec.es.js +273 -0
  38. package/dist/data/include-spec.es.js.map +1 -0
  39. package/dist/data/paginate.d.ts +8 -1
  40. package/dist/data/paginate.es.js +181 -0
  41. package/dist/data/paginate.es.js.map +1 -0
  42. package/dist/data/query_builder.d.ts +52 -4
  43. package/dist/data/query_builder.es.js +151 -0
  44. package/dist/data/query_builder.es.js.map +1 -0
  45. package/dist/data/resolveDataSource.es.js +87 -0
  46. package/dist/data/resolveDataSource.es.js.map +1 -0
  47. package/dist/data/row-etag.d.ts +46 -0
  48. package/dist/data/row-etag.es.js +64 -0
  49. package/dist/data/row-etag.es.js.map +1 -0
  50. package/dist/data/sort-dialect.es.js +236 -0
  51. package/dist/data/sort-dialect.es.js.map +1 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.es.js +56 -7235
  54. package/dist/table-classification.es.js +107 -0
  55. package/dist/table-classification.es.js.map +1 -0
  56. package/dist/util/auth-default-policies.es.js +184 -0
  57. package/dist/util/auth-default-policies.es.js.map +1 -0
  58. package/dist/util/builders.es.js +15 -0
  59. package/dist/util/builders.es.js.map +1 -0
  60. package/dist/util/callback-errors.es.js +164 -0
  61. package/dist/util/callback-errors.es.js.map +1 -0
  62. package/dist/util/callbacks.es.js +72 -0
  63. package/dist/util/callbacks.es.js.map +1 -0
  64. package/dist/util/collections.es.js +79 -0
  65. package/dist/util/collections.es.js.map +1 -0
  66. package/dist/util/common.es.js +7 -0
  67. package/dist/util/common.es.js.map +1 -0
  68. package/dist/util/conditions.es.js +100 -0
  69. package/dist/util/conditions.es.js.map +1 -0
  70. package/dist/util/copy.d.ts +33 -0
  71. package/dist/util/copy.es.js +74 -0
  72. package/dist/util/copy.es.js.map +1 -0
  73. package/dist/util/download-url-cache.d.ts +122 -0
  74. package/dist/util/download-url-cache.es.js +187 -0
  75. package/dist/util/download-url-cache.es.js.map +1 -0
  76. package/dist/util/email.es.js +36 -0
  77. package/dist/util/email.es.js.map +1 -0
  78. package/dist/util/entities.d.ts +27 -2
  79. package/dist/util/entities.es.js +328 -0
  80. package/dist/util/entities.es.js.map +1 -0
  81. package/dist/util/enums.d.ts +10 -1
  82. package/dist/util/enums.es.js +37 -0
  83. package/dist/util/enums.es.js.map +1 -0
  84. package/dist/util/identity.es.js +138 -0
  85. package/dist/util/identity.es.js.map +1 -0
  86. package/dist/util/index.d.ts +6 -0
  87. package/dist/util/internal-tables.d.ts +10 -0
  88. package/dist/util/internal-tables.es.js +202 -0
  89. package/dist/util/internal-tables.es.js.map +1 -0
  90. package/dist/util/junction-policies.es.js +273 -0
  91. package/dist/util/junction-policies.es.js.map +1 -0
  92. package/dist/util/paths.es.js +26 -0
  93. package/dist/util/paths.es.js.map +1 -0
  94. package/dist/util/permissions.es.js +108 -0
  95. package/dist/util/permissions.es.js.map +1 -0
  96. package/dist/util/pg-column-to-property.es.js +303 -0
  97. package/dist/util/pg-column-to-property.es.js.map +1 -0
  98. package/dist/util/policy/compileRulePolicies.d.ts +32 -0
  99. package/dist/util/policy/compileRulePolicies.es.js +43 -0
  100. package/dist/util/policy/compileRulePolicies.es.js.map +1 -0
  101. package/dist/util/policy/evaluatePolicy.es.js +104 -0
  102. package/dist/util/policy/evaluatePolicy.es.js.map +1 -0
  103. package/dist/util/policy/index.d.ts +1 -0
  104. package/dist/util/policy/policyToPostgres.d.ts +24 -0
  105. package/dist/util/policy/policyToPostgres.es.js +593 -0
  106. package/dist/util/policy/policyToPostgres.es.js.map +1 -0
  107. package/dist/util/policy/policyToSecurityRule.es.js +79 -0
  108. package/dist/util/policy/policyToSecurityRule.es.js.map +1 -0
  109. package/dist/util/policy/securityRuleToConditions.es.js +48 -0
  110. package/dist/util/policy/securityRuleToConditions.es.js.map +1 -0
  111. package/dist/util/policy/sqlToPolicy.es.js +326 -0
  112. package/dist/util/policy/sqlToPolicy.es.js.map +1 -0
  113. package/dist/util/relations.es.js +243 -0
  114. package/dist/util/relations.es.js.map +1 -0
  115. package/dist/util/resolutions.es.js +318 -0
  116. package/dist/util/resolutions.es.js.map +1 -0
  117. package/dist/util/resolve-relation.es.js +163 -0
  118. package/dist/util/resolve-relation.es.js.map +1 -0
  119. package/dist/util/resumable-upload-threshold.d.ts +15 -0
  120. package/dist/util/resumable-upload-threshold.es.js +20 -0
  121. package/dist/util/resumable-upload-threshold.es.js.map +1 -0
  122. package/dist/util/resumable-upload.d.ts +51 -0
  123. package/dist/util/resumable-upload.es.js +121 -0
  124. package/dist/util/resumable-upload.es.js.map +1 -0
  125. package/dist/util/sql-redaction.d.ts +21 -0
  126. package/dist/util/sql-redaction.es.js +107 -0
  127. package/dist/util/sql-redaction.es.js.map +1 -0
  128. package/dist/util/sql-rows.es.js +41 -0
  129. package/dist/util/sql-rows.es.js.map +1 -0
  130. package/dist/util/storage.d.ts +26 -4
  131. package/dist/util/storage.es.js +117 -0
  132. package/dist/util/storage.es.js.map +1 -0
  133. package/dist/util/string-column-length.es.js +31 -0
  134. package/dist/util/string-column-length.es.js.map +1 -0
  135. package/dist/util/tenant.es.js +201 -0
  136. package/dist/util/tenant.es.js.map +1 -0
  137. package/dist/util/untrusted-envelope.d.ts +25 -0
  138. package/dist/util/untrusted-envelope.es.js +40 -0
  139. package/dist/util/untrusted-envelope.es.js.map +1 -0
  140. package/package.json +5 -3
  141. package/dist/index.es.js.map +0 -1
@@ -0,0 +1,786 @@
1
+ import { normalizeToEntityRelation } from "../util/entities.es.js";
2
+ import { ALL_WHERE_FILTER_OPS, CANONICAL_TO_REST, LIST_OPS, NULL_OPS, REST_TO_CANONICAL, toCanonicalOp } from "@rebasepro/types";
3
+ //#region src/data/filter-dialect.ts
4
+ /**
5
+ * REST wire-format adapter for the unified filter system.
6
+ *
7
+ * This module is the ONLY code in the entire codebase that knows about
8
+ * PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
9
+ * Everything else speaks `FilterValues` exclusively.
10
+ *
11
+ * Wire-format values are always strings — the wire format carries no type
12
+ * metadata, so type coercion is the responsibility of the server-side data
13
+ * driver which has access to the collection schema.
14
+ *
15
+ * Structural characters inside a value are backslash-escaped: `,` → `\,`,
16
+ * `(` → `\(`, `)` → `\)`, and a literal backslash as `\\`. Decoding is
17
+ * deliberately conservative — only those four sequences are decoded, so a
18
+ * backslash that arrives unescaped from an older client survives intact.
19
+ *
20
+ * @module
21
+ */
22
+ /**
23
+ * Serialize a JS value to its querystring representation.
24
+ * `null` is serialized as the literal string `"null"`.
25
+ * Relation values (`EntityRelation` instances or `{ __type: "relation", id, path }`
26
+ * objects) are serialized as their raw id — the wire format only carries the
27
+ * value to compare against the FK column.
28
+ *
29
+ * A `Date` is its ISO 8601 instant — what JSON makes of it on the socket and
30
+ * in-process, so one filter names one instant through every door. `String()`
31
+ * gave the viewer's local zone, no milliseconds and a parenthesised zone name
32
+ * Postgres cannot parse. An invalid date keeps its "Invalid Date" spelling for
33
+ * the server to refuse, rather than throwing a `RangeError` from here.
34
+ */
35
+ function stringifyValue(value) {
36
+ if (value === null) return "null";
37
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
38
+ const relation = normalizeToEntityRelation(value);
39
+ if (relation) return String(relation.id);
40
+ return String(value);
41
+ }
42
+ /**
43
+ * Characters that carry structure in the wire format and must therefore be
44
+ * escaped inside a value: the separator, the group delimiters, and the escape
45
+ * character itself.
46
+ *
47
+ * Parentheses are here because `and(...)`/`or(...)` groups are parsed by
48
+ * tracking paren depth. A value containing one is not merely ambiguous, it
49
+ * moves where the parser thinks the group ends.
50
+ */
51
+ var WIRE_SPECIALS = /[\\,()]/g;
52
+ /**
53
+ * Escape a value for the wire format: `\` → `\\`, `,` → `\,`, `(` → `\(`,
54
+ * `)` → `\)`.
55
+ */
56
+ /**
57
+ * The wire spelling of an empty list.
58
+ *
59
+ * A lone backslash: unproducible by {@link escapeWireValue}, which doubles
60
+ * every backslash it emits, so it cannot collide with any real item.
61
+ */
62
+ var EMPTY_LIST_TOKEN = "\\";
63
+ function escapeWireValue(value) {
64
+ return value.replace(WIRE_SPECIALS, (ch) => `\\${ch}`);
65
+ }
66
+ /**
67
+ * Unescape a wire-format value.
68
+ *
69
+ * **Conservative**, and deliberately so: only the four sequences
70
+ * {@link escapeWireValue} actually produces are decoded. A backslash followed
71
+ * by anything else is left exactly as it is.
72
+ *
73
+ * This used to consume the backslash before *any* character, which is
74
+ * indistinguishable for anything this codec emitted — it only ever emits those
75
+ * four — but not for input arriving from elsewhere. A client on an older
76
+ * release sends a Windows path or a LIKE pattern with a literal `C:\x`
77
+ * unescaped, and greedy unescaping silently turned it into `C:x`, changing
78
+ * which rows matched. Decoding only what the encoder can produce makes the two
79
+ * directions agree across versions.
80
+ */
81
+ function unescapeWireValue(value) {
82
+ let result = "";
83
+ for (let i = 0; i < value.length; i++) {
84
+ const next = value[i + 1];
85
+ if (value[i] === "\\" && (next === "\\" || next === "," || next === "(" || next === ")")) {
86
+ result += next;
87
+ i++;
88
+ continue;
89
+ }
90
+ result += value[i];
91
+ }
92
+ return result;
93
+ }
94
+ /**
95
+ * Whether a scalar, sent raw as a query parameter's operand, would be read
96
+ * back as something other than itself.
97
+ *
98
+ * A query parameter owns its whole value, so its scalar goes out unescaped —
99
+ * `eq.a,b`, `eq.C:\x` — and the decoder returns it verbatim. Two shapes break
100
+ * that. A parenthesised operand is a list for every operator (so that
101
+ * `["==", ["a","b"]]` on an array column stays a list), so raw, the scalar
102
+ * `"(deleted)"` would arrive as `["deleted"]`: an `==` matching nothing, a
103
+ * `!=` returning the very rows it excludes. Such a scalar is sent escaped,
104
+ * `eq.\(deleted\)`. And that spelling has to be told apart from a raw value
105
+ * that looks the same, so a scalar of *that* shape is escaped in turn.
106
+ *
107
+ * Everything else stays raw, so a value an older client sends decodes as it
108
+ * always has — unless it is exactly the escaped spelling of such a scalar,
109
+ * which decodes to the scalar it spells.
110
+ */
111
+ function scalarNeedsEscape(value) {
112
+ if (value.startsWith("(") && value.endsWith(")")) return true;
113
+ return decodeEscapedScalar(value) !== void 0;
114
+ }
115
+ /**
116
+ * The scalar an operand is the escaped spelling of, when it is one.
117
+ *
118
+ * Exact on purpose: only an operand that {@link escapeWireValue} produces from
119
+ * a scalar that {@link scalarNeedsEscape} is decoded. Any other operand —
120
+ * including a raw backslash an older client sent, like `\\server\share` — is
121
+ * left verbatim. Terminates because an operand that matches contains an
122
+ * escape pair, so the value is strictly shorter than it.
123
+ */
124
+ function decodeEscapedScalar(operand) {
125
+ if (!operand.startsWith("\\")) return void 0;
126
+ const value = unescapeWireValue(operand);
127
+ return escapeWireValue(value) === operand && scalarNeedsEscape(value) ? value : void 0;
128
+ }
129
+ /**
130
+ * Split a parenthesized list string on unescaped commas.
131
+ * Input is the content between `(` and `)`.
132
+ *
133
+ * @example
134
+ * splitListItems("admin,editor") // ["admin", "editor"]
135
+ * splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
136
+ */
137
+ function splitListItems(inner) {
138
+ const items = [];
139
+ let current = "";
140
+ for (let i = 0; i < inner.length; i++) if (inner[i] === "\\" && i + 1 < inner.length) {
141
+ current += inner[i] + inner[i + 1];
142
+ i++;
143
+ } else if (inner[i] === ",") {
144
+ items.push(unescapeWireValue(current));
145
+ current = "";
146
+ } else current += inner[i];
147
+ items.push(unescapeWireValue(current));
148
+ return items;
149
+ }
150
+ /**
151
+ * Split a group body on commas at paren depth 0, honouring escapes.
152
+ *
153
+ * The escape-awareness is the point. The splitter used to track only paren
154
+ * depth, so a comma inside a scalar value ended a condition:
155
+ * `or(name.eq.Doe, John,age.gte.18)` parsed as *three* conditions, the middle
156
+ * one a fabricated `" John" == true`. On an `or` that widens the result set,
157
+ * and nothing anywhere reports an error — the query simply stops meaning what
158
+ * the caller wrote.
159
+ */
160
+ function splitGroupItems(inner) {
161
+ const parts = [];
162
+ let depth = 0;
163
+ let start = 0;
164
+ for (let i = 0; i < inner.length; i++) {
165
+ const ch = inner[i];
166
+ if (ch === "\\" && i + 1 < inner.length) {
167
+ i++;
168
+ continue;
169
+ }
170
+ if (ch === "(") depth++;
171
+ else if (ch === ")") depth--;
172
+ else if (ch === "," && depth === 0) {
173
+ parts.push(inner.slice(start, i));
174
+ start = i + 1;
175
+ }
176
+ }
177
+ parts.push(inner.slice(start));
178
+ return parts;
179
+ }
180
+ /**
181
+ * Operator tables as `Map`s, because the key comes off the wire.
182
+ *
183
+ * Indexed as plain objects, every `Object.prototype` member answered: a query
184
+ * string of `?f=valueOf.x` found a truthy "operator" — the inherited function —
185
+ * and `deserializeTuple` returned it *as the operator*, so a function object
186
+ * travelled on into the compilers in place of a `WhereFilterOp`. The guard one
187
+ * line below (`if (!canonicalOp)`) reads as though it rejects anything unknown,
188
+ * and does not: `Object.prototype` is not unknown to a plain object.
189
+ *
190
+ * Same shape as the prototype-key defects swept out of `setIn`, `getIn`,
191
+ * `mergeDeep`, `unflattenObject` and `FOREIGN_CONVENTION_UIDS`.
192
+ */
193
+ var REST_OP_LOOKUP = new Map(Object.entries(REST_TO_CANONICAL));
194
+ var CANONICAL_OP_LOOKUP = new Map(Object.entries(CANONICAL_TO_REST));
195
+ /** The operator spellings a rejection lists back to the caller. */
196
+ var VALID_OPERATOR_LIST = ALL_WHERE_FILTER_OPS.join(", ");
197
+ /**
198
+ * A filter condition named an operator this dialect does not have.
199
+ *
200
+ * ## Why this throws, rather than returning a typed rejection
201
+ *
202
+ * `deserializeFilter` is the *shared* codec: the REST ingress
203
+ * (`packages/server/src/api/rest/query-parser.ts`), the browser SDK and the
204
+ * admin panel (`buildRebaseData.ts`) all decode through it. Two constraints
205
+ * follow.
206
+ *
207
+ * - It cannot throw the server's `ApiError`. `@rebasepro/common` does not
208
+ * depend on `@rebasepro/server` (the dependency runs the other way), and a
209
+ * browser client has no error handler to render an `ApiError` with. So the
210
+ * rejection is this plain `Error` subclass, whose `message` reads correctly
211
+ * wherever it surfaces — a rejected promise in an app, a 400 body over HTTP.
212
+ * - It cannot be a returned rejection *value*. Every caller assigns the result
213
+ * straight into a query it is about to run; a sentinel that none of them
214
+ * check would be ignored, which is exactly the silently-wrong-filter failure
215
+ * this exists to stop. Throwing is also what this file already does for the
216
+ * sibling cases — `serializeTuple` on an unknown canonical operator,
217
+ * `deserializeLogicalCondition` past the nesting bound — and the REST parser
218
+ * already converts the latter into a 400.
219
+ *
220
+ * `statusCode`, `code` and `details` are carried as fields because the server's
221
+ * Hono error handler duck-types those off any thrown error: a decode path that
222
+ * forgets to convert still answers 400 with the canonical envelope instead of a
223
+ * 500 that says "An unexpected error occurred". `query-parser.ts` converts
224
+ * explicitly all the same — that is the path the contract is stated on, and an
225
+ * incidental 400 is not a contract.
226
+ */
227
+ var UnknownFilterOperatorError = class extends Error {
228
+ /** The field the condition was written against. */
229
+ field;
230
+ /** The operator string as it arrived, verbatim. */
231
+ operator;
232
+ /** Every operator this dialect accepts, in canonical spelling. */
233
+ validOperators = ALL_WHERE_FILTER_OPS;
234
+ /** See the class docblock: read by the server's error handler. */
235
+ statusCode = 400;
236
+ code = "UNKNOWN_FILTER_OPERATOR";
237
+ details;
238
+ constructor(field, operator) {
239
+ super(`Unknown filter operator '${operator}' on field '${field}'. Valid operators: ${VALID_OPERATOR_LIST}`);
240
+ this.name = "UnknownFilterOperatorError";
241
+ this.field = field;
242
+ this.operator = operator;
243
+ this.details = {
244
+ field,
245
+ operator,
246
+ validOperators: ALL_WHERE_FILTER_OPS
247
+ };
248
+ }
249
+ };
250
+ /**
251
+ * Two to three characters of ASCII punctuation and nothing else — the shape
252
+ * every symbolic operator has (`==`, `>=`, `<>`, `~~`, `!!`, `>>`, `===`), and
253
+ * one a column value effectively never has.
254
+ *
255
+ * Two characters minimum on purpose. A *single* punctuation character is a
256
+ * perfectly ordinary value — `{ grade: ["-", "+"] }` is a two-item list, not a
257
+ * condition — and the only single-character operator anyone actually mistypes
258
+ * is `=`, which is named separately below. `<` and `>` need no special case:
259
+ * they are real operators and resolve.
260
+ */
261
+ var SYMBOLIC_OPERATOR = /^[^\p{L}\p{N}\s]{2,3}$/u;
262
+ /** Lowercase, strip everything that is not a letter or digit. */
263
+ function normalizeOperatorName(op) {
264
+ return op.toLowerCase().replace(/[^a-z0-9]/g, "");
265
+ }
266
+ /**
267
+ * Every real operator name with its case and separators removed, so a
268
+ * respelling of one — `arrayContains`, `not_in`, `NOT-LIKE`, `isNull` — is
269
+ * recognised as an attempt at an operator rather than read as a value.
270
+ *
271
+ * These are rejected rather than accepted: admitting a second spelling of an
272
+ * operator would leave two wire spellings of one thing, and the rejection
273
+ * message names the one that works.
274
+ */
275
+ var RESPELLED_OPERATORS = new Set([...ALL_WHERE_FILTER_OPS, ...Object.keys(REST_TO_CANONICAL)].map(normalizeOperatorName));
276
+ /**
277
+ * Operator names *other* query dialects use, which this one does not have.
278
+ *
279
+ * This list is curated, and deliberately so. For a word-shaped string there is
280
+ * no rule that separates "an operator the caller guessed" from "a value that
281
+ * happens to be a word": `{ tags: ["a", "b"] }` has to keep meaning a two-item
282
+ * `in` list, so the codec cannot simply refuse every unrecognised word in
283
+ * position 0. The line is therefore drawn by name, and only around names whose
284
+ * use as an operator is far more likely than their use as one of two sibling
285
+ * values. `contains` is the motivating case — the first thing a developer
286
+ * reaches for, and until now it compiled to `title IN ('contains', 'Hell')`.
287
+ *
288
+ * Genuinely ambiguous single words (`any`, `all`, `exists`, `search`, `not`)
289
+ * are left off: as operators they are rare, and as enum values they are common.
290
+ * Everywhere else the tie goes to *rejecting*, because a 400 naming the
291
+ * supported set costs the caller one round trip, and the alternative — which is
292
+ * what every name on this list used to produce — is a query that runs, returns
293
+ * rows, and is wrong.
294
+ */
295
+ var NEAR_MISS_OPERATORS = /* @__PURE__ */ new Set([
296
+ "contains",
297
+ "notcontains",
298
+ "doesnotcontain",
299
+ "doesnotcontains",
300
+ "includes",
301
+ "notincludes",
302
+ "startswith",
303
+ "notstartswith",
304
+ "beginswith",
305
+ "startingwith",
306
+ "endswith",
307
+ "notendswith",
308
+ "matches",
309
+ "notmatches",
310
+ "regex",
311
+ "regexp",
312
+ "between",
313
+ "notbetween",
314
+ "equals",
315
+ "notequals",
316
+ "equalto",
317
+ "isequalto",
318
+ "isnotequalto",
319
+ "greaterthan",
320
+ "greaterthanorequal",
321
+ "greaterthanorequalto",
322
+ "lessthan",
323
+ "lessthanorequal",
324
+ "lessthanorequalto",
325
+ "isempty",
326
+ "isnotempty",
327
+ "oneof",
328
+ "noneof",
329
+ "anyof",
330
+ "allof",
331
+ "null",
332
+ "isnullorempty"
333
+ ]);
334
+ /**
335
+ * Was this string *meant* as an operator?
336
+ *
337
+ * Only consulted after {@link toCanonicalOp} has already failed to resolve it,
338
+ * so a `true` here is always a rejection.
339
+ */
340
+ function isOperatorShaped(op) {
341
+ if (op === "=") return true;
342
+ if (SYMBOLIC_OPERATOR.test(op)) return true;
343
+ const normalized = normalizeOperatorName(op);
344
+ if (!normalized) return false;
345
+ return RESPELLED_OPERATORS.has(normalized) || NEAR_MISS_OPERATORS.has(normalized);
346
+ }
347
+ /**
348
+ * Read a `[op, value]` tuple, if that is what this is.
349
+ *
350
+ * Three outcomes, and the middle one is the defect this function exists for:
351
+ *
352
+ * - the operator resolves (canonical *or* REST spelling) → the canonical tuple;
353
+ * - the operator does not resolve but was plainly meant as one → throw;
354
+ * - it does not look like an operator at all → `undefined`, and the caller
355
+ * falls back to reading the array as a list of values.
356
+ *
357
+ * The old test was `toCanonicalOp(raw[0]) === raw[0]`, i.e. canonical spelling
358
+ * only, with *everything else* — including every REST short-code — dropping
359
+ * through to `["in", raw]`. So the operator string itself became a value in a
360
+ * membership test: `["!!", "Hello"]` compiled to `title IN ('!!','Hello')`,
361
+ * which matches, and the caller got back rows their filter was written to
362
+ * exclude. `["eq", "active"]` had the same shape of failure.
363
+ */
364
+ function readTuple(field, raw) {
365
+ if (!Array.isArray(raw) || raw.length !== 2) return void 0;
366
+ const [op, value] = raw;
367
+ if (typeof op !== "string") return void 0;
368
+ const canonical = toCanonicalOp(op);
369
+ if (canonical) return [canonical, value];
370
+ if (op.includes(".")) return void 0;
371
+ if (isOperatorShaped(op)) throw new UnknownFilterOperatorError(field, op);
372
+ }
373
+ /**
374
+ * The query-parameter names a REST listing reads as something other than a
375
+ * field filter: pagination and whether to count, sorting, includes,
376
+ * projection, search, the logical groups, the JSON `where` dialect, an
377
+ * aggregate's `select`/`groupBy`, and the soft-delete switches.
378
+ *
379
+ * One list for both ends. The server skips these names when it collects
380
+ * `?<field>=<op>.<value>` filters, and the SDK sends a filter on a column with
381
+ * one of these names inside `?where=` instead — a collection is free to have a
382
+ * `page`, `select` or `hard` column, and a filter on it sent as its own
383
+ * parameter was read as the parameter and never applied.
384
+ */
385
+ var RESERVED_QUERY_KEYS = /* @__PURE__ */ new Set([
386
+ "limit",
387
+ "offset",
388
+ "page",
389
+ "after",
390
+ "orderBy",
391
+ "include",
392
+ "fields",
393
+ "distinct",
394
+ "count",
395
+ "searchString",
396
+ "searchExplain",
397
+ "vector_search",
398
+ "vector",
399
+ "vector_distance",
400
+ "vector_threshold",
401
+ "or",
402
+ "and",
403
+ "not",
404
+ "where",
405
+ "select",
406
+ "groupBy",
407
+ "deleted",
408
+ "hard"
409
+ ]);
410
+ /**
411
+ * Encode the `<op>.<value>` half of a wire condition.
412
+ *
413
+ * This is the single leaf encoder. Both wire positions that carry a condition
414
+ * — a top-level query parameter (`?status=eq.active`) and a leaf inside an
415
+ * `and(...)`/`or(...)` group (`or(status.eq.active,…)`) — go through it, so a
416
+ * rule expressed here holds in both. The group serializer used to carry its
417
+ * own copy, and the copy had drifted on every rule that matters: `null` went
418
+ * out as the four-character string, the empty list as `()`, and an operator
419
+ * this dialect does not have was silently rewritten to `eq` — a filter that
420
+ * ran, returned rows, and answered a different question than the one asked.
421
+ *
422
+ * `escapeScalar` is the one thing the two positions legitimately disagree
423
+ * about. A scalar in a query parameter owns the whole value and needs no
424
+ * escaping; a scalar inside a group sits between the same commas a list item
425
+ * does, so a comma in it would end the condition early.
426
+ */
427
+ function serializeOperatorAndValue(op, value, { escapeScalar, where }) {
428
+ if (typeof op !== "string") throw new TypeError(`${where}: operator must be a string, got ${typeof op}`);
429
+ const restOp = CANONICAL_OP_LOOKUP.get(op);
430
+ if (!restOp) throw new TypeError(`${where}: unknown operator "${op}". Valid operators: ${Object.keys(CANONICAL_TO_REST).join(", ")}`);
431
+ if (value === null && (op === "==" || op === "!=")) return op === "==" ? "isnull.null" : "notnull.null";
432
+ if (NULL_OPS.has(op)) return `${restOp}.null`;
433
+ if (Array.isArray(value)) {
434
+ if (value.length === 0) return `${restOp}.(${EMPTY_LIST_TOKEN})`;
435
+ return `${restOp}.(${value.map((v) => escapeWireValue(stringifyValue(v))).join(",")})`;
436
+ }
437
+ const scalar = stringifyValue(value);
438
+ return `${restOp}.${escapeScalar || !LIST_OPS.has(op) && scalarNeedsEscape(scalar) ? escapeWireValue(scalar) : scalar}`;
439
+ }
440
+ /**
441
+ * Serialize a single canonical condition tuple to a PostgREST dot-string.
442
+ *
443
+ * Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
444
+ *
445
+ * @example
446
+ * serializeTuple(["==", "active"]) // "eq.active"
447
+ * serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
448
+ * serializeTuple([">=", 18]) // "gte.18"
449
+ */
450
+ function serializeTuple(tuple) {
451
+ if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
452
+ const [op, value] = tuple;
453
+ return serializeOperatorAndValue(op, value, {
454
+ escapeScalar: false,
455
+ where: "serializeTuple"
456
+ });
457
+ }
458
+ /**
459
+ * Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
460
+ * querystring record.
461
+ *
462
+ * - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
463
+ * - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
464
+ * - Single conditions produce a string value.
465
+ * - Multiple conditions on the same field produce a string array (repeated params).
466
+ *
467
+ * @example
468
+ * serializeFilter({ status: ["==", "active"] })
469
+ * // → { status: "eq.active" }
470
+ *
471
+ * serializeFilter({ age: [[">=", 18], ["<", 65]] })
472
+ * // → { age: ["gte.18", "lt.65"] }
473
+ *
474
+ * // Pre-serialized strings pass through unchanged:
475
+ * serializeFilter({ status: "eq.published" })
476
+ * // → { status: "eq.published" }
477
+ */
478
+ function serializeFilter(filter) {
479
+ const result = {};
480
+ for (const [field, condition] of Object.entries(filter)) {
481
+ if (condition === void 0) continue;
482
+ if (typeof condition === "string") {
483
+ result[field] = condition;
484
+ continue;
485
+ }
486
+ if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
487
+ else result[field] = serializeTuple(condition);
488
+ }
489
+ return result;
490
+ }
491
+ /**
492
+ * The spellings a null-testing operator's operand may take.
493
+ *
494
+ * The serializer writes `isnull.null`; a hand-written `isnull.true` means the
495
+ * same thing and has always been accepted. Anything else after the operator is
496
+ * not an operand it has — `notnull.reason` is a *value* — see
497
+ * {@link deserializeSingle}.
498
+ */
499
+ var NULL_OPERANDS = /* @__PURE__ */ new Set([
500
+ "null",
501
+ "true",
502
+ "false",
503
+ ""
504
+ ]);
505
+ /**
506
+ * Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
507
+ *
508
+ * All values are returned as strings — the wire format carries no type
509
+ * metadata, so coercion is the data driver's responsibility.
510
+ *
511
+ * ## When a leading segment is an operator, and when it is part of the value
512
+ *
513
+ * `?status=in.progress` and `?status=in.(a,b)` differ by one character and mean
514
+ * entirely different things, and the reading here decides which. The rule, in
515
+ * full:
516
+ *
517
+ * > A dot-string is read as `operator.operand` **only** when its first segment
518
+ * > names a known REST operator **and** what follows is a well-formed operand
519
+ * > *for that operator's arity*. Otherwise the whole string is the value.
520
+ *
521
+ * Arity, per operator family:
522
+ *
523
+ * - **List** operators (`in`, `nin`, `csa` — `LIST_OPS`) take a parenthesised
524
+ * list and nothing else. `in.(draft,review)` is the operator; `in.progress`
525
+ * is the *value* `"in.progress"`, because there is no list there and so no
526
+ * `in` filter that could have been written. That case used to compile to
527
+ * `status IN ('progress')` — a filter the caller never wrote, quietly
528
+ * matching the wrong rows and, on a status field, hiding every row they were
529
+ * looking for.
530
+ * - **Null** operators (`isnull`, `notnull` — {@link NULL_OPS}) take no
531
+ * operand: only {@link NULL_OPERANDS}. `notnull.reason` is the value
532
+ * `"notnull.reason"`, not "reason is not null".
533
+ * - **Everything else** takes one scalar, and any remainder is one — including
534
+ * the empty string, so `eq.` really is "equals the empty string". A
535
+ * parenthesised remainder is a list for these too (`eq.(a,b)` is array
536
+ * equality); a scalar that is itself parenthesised arrives escaped,
537
+ * `eq.\(none\)`, and is decoded here — see {@link scalarNeedsEscape}.
538
+ *
539
+ * ### The one ambiguity that remains, and how to write past it
540
+ *
541
+ * A scalar operator's operand is unconstrained, so `?status=like.that` is a
542
+ * `LIKE 'that'` and no rule at this layer can tell it from the literal value
543
+ * `"like.that"` — both are well-formed encodings, and picking either by guess
544
+ * would break the other. Two spellings say "value" unambiguously, and both
545
+ * round-trip:
546
+ *
547
+ * - `?status=eq.like.that` — name the operator. The *first* segment is consumed
548
+ * as the operator and everything after it is the value, dots and all. This is
549
+ * what `serializeFilter` emits, which is why the SDK never meets the
550
+ * ambiguity at all.
551
+ * - `?where={"status":["==","like.that"]}` — the JSON dialect's tuple form.
552
+ *
553
+ * Values that merely *contain* dots (`user@host.com`, `1.2.3`) were never
554
+ * ambiguous: their first segment names no operator to begin with.
555
+ */
556
+ function deserializeSingle(raw) {
557
+ const dotIndex = raw.indexOf(".");
558
+ if (dotIndex === -1) return ["==", raw];
559
+ const prefix = raw.substring(0, dotIndex);
560
+ const rest = raw.substring(dotIndex + 1);
561
+ const canonicalOp = REST_OP_LOOKUP.get(prefix);
562
+ if (!canonicalOp) return ["==", raw];
563
+ if (NULL_OPS.has(canonicalOp)) {
564
+ if (!NULL_OPERANDS.has(rest)) return ["==", raw];
565
+ return [canonicalOp, null];
566
+ }
567
+ if (rest.startsWith("(") && rest.endsWith(")")) {
568
+ const inner = rest.slice(1, -1);
569
+ return [canonicalOp, inner === EMPTY_LIST_TOKEN ? [] : splitListItems(inner)];
570
+ }
571
+ if (LIST_OPS.has(canonicalOp)) return ["==", raw];
572
+ return [canonicalOp, decodeEscapedScalar(rest) ?? rest];
573
+ }
574
+ /**
575
+ * Convert a PostgREST-style querystring record to `FilterValues`.
576
+ *
577
+ * - String values are parsed as single conditions.
578
+ * - String arrays (repeated query params) become multiple conditions on the same field.
579
+ *
580
+ * @example
581
+ * deserializeFilter({ status: "eq.active" })
582
+ * // → { status: ["==", "active"] }
583
+ *
584
+ * deserializeFilter({ age: ["gte.18", "lt.65"] })
585
+ * // → { age: [[">=", "18"], ["<", "65"]] }
586
+ *
587
+ * @throws {UnknownFilterOperatorError} when a condition names an operator this
588
+ * dialect does not have. See that class for why a rejection here is a throw.
589
+ */
590
+ function deserializeFilter(query) {
591
+ const result = {};
592
+ for (const [field, raw] of Object.entries(query)) {
593
+ if (raw === void 0) continue;
594
+ const tuple = readTuple(field, raw);
595
+ if (tuple) {
596
+ result[field] = tuple;
597
+ continue;
598
+ }
599
+ if (Array.isArray(raw)) {
600
+ if (raw.length === 0) continue;
601
+ if (Array.isArray(raw[0])) {
602
+ const tuples = raw.map((item) => readTuple(field, item));
603
+ if (tuples.every((t) => t !== void 0)) {
604
+ result[field] = tuples;
605
+ continue;
606
+ }
607
+ }
608
+ if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
609
+ else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
610
+ else result[field] = ["in", raw];
611
+ } else if (typeof raw === "string") result[field] = deserializeSingle(raw);
612
+ else result[field] = ["==", raw];
613
+ }
614
+ return result;
615
+ }
616
+ /**
617
+ * Serialize a `LogicalCondition` or `FilterCondition` to its wire-format string.
618
+ *
619
+ * Leaf encoding is {@link serializeOperatorAndValue}, the same function
620
+ * `serializeTuple` uses, so `null`, the empty list and an unknown operator
621
+ * behave identically inside a group and in a query parameter.
622
+ *
623
+ * @throws {TypeError} when a leaf names an operator this dialect does not have.
624
+ * It used to fall back to `eq`, which turned `age >= 18` into `age = 18` with
625
+ * no diagnostic anywhere.
626
+ *
627
+ * @example
628
+ * serializeLogicalCondition({ column: "status", operator: "==", value: "active" })
629
+ * // → "status.eq.active"
630
+ *
631
+ * serializeLogicalCondition({ column: "deleted_at", operator: "==", value: null })
632
+ * // → "deleted_at.isnull.null"
633
+ *
634
+ * serializeLogicalCondition({ type: "or", conditions: [...] })
635
+ * // → "or(status.eq.active,status.eq.pending)"
636
+ */
637
+ function serializeLogicalCondition(cond) {
638
+ if ("type" in cond) {
639
+ const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
640
+ return `${cond.type}(${inner})`;
641
+ }
642
+ return `${escapeWireValue(cond.column)}.${serializeOperatorAndValue(cond.operator, cond.value, {
643
+ escapeScalar: true,
644
+ where: "serializeLogicalCondition"
645
+ })}`;
646
+ }
647
+ /**
648
+ * The positions of the dots that can separate a leaf's segments.
649
+ *
650
+ * Dots inside a list value (`in.(1.5,2.5)`) are not separators. The value
651
+ * always follows the operator, so the search only needs the region before the
652
+ * first unescaped paren.
653
+ */
654
+ function leafSeparators(str) {
655
+ let limit = str.length;
656
+ for (let i = 0; i < str.length; i++) {
657
+ if (str[i] === "\\") {
658
+ i++;
659
+ continue;
660
+ }
661
+ if (str[i] === "(") {
662
+ limit = i;
663
+ break;
664
+ }
665
+ }
666
+ const dots = [];
667
+ for (let i = 0; i < limit; i++) {
668
+ if (str[i] === "\\") {
669
+ i++;
670
+ continue;
671
+ }
672
+ if (str[i] === ".") dots.push(i);
673
+ }
674
+ return dots;
675
+ }
676
+ /**
677
+ * Split a leaf condition into `column`, operator token and value.
678
+ *
679
+ * The naive reading — column is everything before the first dot, operator is
680
+ * everything up to the second — cannot express a relation path. A filter on
681
+ * `author.name` serializes to `author.name.eq.bob` and came back as the column
682
+ * `author` with the operator `name`, which resolves to nothing, so the
683
+ * fallback made it `author == "eq.bob"`: a condition that runs and matches
684
+ * nothing, on a column the caller never named.
685
+ *
686
+ * So the operator is found rather than assumed: it is the first dot-separated
687
+ * segment after the column that resolves to a real operator. Everything before
688
+ * it is the column, everything after is the value. `version.eq.1.2.3` reads as
689
+ * `version == "1.2.3"` because the scan stops at the first match — the `eq` at
690
+ * offset 1, not a later segment — and `metadata->>x.eq.5` never had dots in the
691
+ * column to begin with.
692
+ *
693
+ * Returns `undefined` when no segment resolves — `status.active`, an equality
694
+ * written without an operator, which the caller handles.
695
+ */
696
+ function splitLeafCondition(str) {
697
+ const dots = leafSeparators(str);
698
+ for (let i = 1; i < dots.length; i++) {
699
+ const operator = toCanonicalOp(str.substring(dots[i - 1] + 1, dots[i]));
700
+ if (!operator) continue;
701
+ return {
702
+ column: unescapeWireValue(str.substring(0, dots[i - 1])),
703
+ operator,
704
+ value: str.substring(dots[i] + 1)
705
+ };
706
+ }
707
+ }
708
+ /**
709
+ * Parse a logical condition wire-format string back into a
710
+ * `LogicalCondition` or `FilterCondition`.
711
+ *
712
+ * @example
713
+ * deserializeLogicalCondition("status.eq.active")
714
+ * // → { column: "status", operator: "==", value: "active" }
715
+ *
716
+ * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
717
+ * // → { type: "or", conditions: [...] }
718
+ */
719
+ /**
720
+ * How deeply `or(...)`/`and(...)` groups may nest.
721
+ *
722
+ * This parser recurses once per level, on a value that arrives in a query
723
+ * string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
724
+ * stack size exceeded`, which a caller sees as a 500 about the call stack
725
+ * rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
726
+ * below that in practice, but "the HTTP layer happens to stop it" is not a
727
+ * bound this parser should rely on.
728
+ *
729
+ * Thirty-two is far past anything a real filter expresses; the deepest in this
730
+ * repository's own tests is three.
731
+ */
732
+ var MAX_LOGICAL_NESTING_DEPTH = 32;
733
+ function deserializeLogicalCondition(str, nesting = 0) {
734
+ if (nesting > MAX_LOGICAL_NESTING_DEPTH) throw new Error(`Filter groups nest more than ${MAX_LOGICAL_NESTING_DEPTH} levels deep. Flatten the condition — \`or(a,or(b,c))\` is \`or(a,b,c)\`.`);
735
+ const logicalMatch = str.match(/^(and|or|not)\((.+)\)$/);
736
+ if (logicalMatch) {
737
+ const type = logicalMatch[1];
738
+ const innerStr = logicalMatch[2];
739
+ return {
740
+ type,
741
+ conditions: splitGroupItems(innerStr).map((part) => deserializeLogicalCondition(part, nesting + 1))
742
+ };
743
+ }
744
+ const leaf = splitLeafCondition(str);
745
+ if (!leaf) {
746
+ const firstDot = str.indexOf(".");
747
+ if (firstDot === -1) return {
748
+ column: unescapeWireValue(str),
749
+ operator: "==",
750
+ value: true
751
+ };
752
+ const dots = leafSeparators(str);
753
+ for (let i = 1; i < dots.length; i++) {
754
+ const segment = str.substring(dots[i - 1] + 1, dots[i]);
755
+ if (isOperatorShaped(segment)) throw new UnknownFilterOperatorError(unescapeWireValue(str.substring(0, dots[i - 1])), segment);
756
+ }
757
+ return {
758
+ column: unescapeWireValue(str.substring(0, firstDot)),
759
+ operator: "==",
760
+ value: unescapeWireValue(str.substring(firstDot + 1))
761
+ };
762
+ }
763
+ const { column, operator, value: valueStr } = leaf;
764
+ if (NULL_OPS.has(operator)) return {
765
+ column,
766
+ operator,
767
+ value: null
768
+ };
769
+ if (valueStr.startsWith("(") && valueStr.endsWith(")")) {
770
+ const inner = valueStr.slice(1, -1);
771
+ return {
772
+ column,
773
+ operator,
774
+ value: inner === EMPTY_LIST_TOKEN ? [] : splitListItems(inner)
775
+ };
776
+ }
777
+ return {
778
+ column,
779
+ operator,
780
+ value: unescapeWireValue(valueStr)
781
+ };
782
+ }
783
+ //#endregion
784
+ export { MAX_LOGICAL_NESTING_DEPTH, RESERVED_QUERY_KEYS, UnknownFilterOperatorError, deserializeFilter, deserializeLogicalCondition, serializeFilter, serializeLogicalCondition };
785
+
786
+ //# sourceMappingURL=filter-dialect.es.js.map