@rebasepro/common 0.13.1-canary.gf57a27e → 0.14.0

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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The `FilterValues` grammar, one level below the wire codec.
3
+ *
4
+ * A field's filter is either one `[op, value]` tuple or an **array** of them —
5
+ * `{ age: [[">=", 18], ["<", 65]] }` — which is what the fluent builder produces
6
+ * from two `.where()` calls on the same column. Reading that shape is grammar,
7
+ * not a driver detail, so every compiler reads it through here.
8
+ *
9
+ * It lived only inside the Postgres compiler, and the Mongo one destructured
10
+ * `const [op, value] = filterParam` regardless: given the array-of-tuples form
11
+ * `op` bound to `[">=", 18]`, no operator matched, and the condition was
12
+ * dropped. Both of them. A read asking for adults under 65 returned every row
13
+ * of the collection with a 200.
14
+ *
15
+ * @module
16
+ */
17
+ import type { WhereFilterOp } from "@rebasepro/types";
18
+ /** One `[operator, value]` condition. */
19
+ export type FilterTuple = [WhereFilterOp, unknown];
20
+ /**
21
+ * Read one field's filter as the list of conditions it stands for.
22
+ *
23
+ * Accepts both declared shapes and normalises them to a list:
24
+ *
25
+ * ```ts
26
+ * toFilterTuples(["==", "active"]) // [["==", "active"]]
27
+ * toFilterTuples([[">=", 18], ["<", 65]]) // [[">=", 18], ["<", 65]]
28
+ * ```
29
+ *
30
+ * A falsy, non-array or empty param has no conditions in it — the empty list,
31
+ * so a caller iterating adds nothing rather than compiling a tuple of
32
+ * `undefined`s and logging about an operator nobody sent.
33
+ */
34
+ export declare function toFilterTuples(filterParam: unknown): FilterTuple[];
@@ -9,12 +9,61 @@
9
9
  * metadata, so type coercion is the responsibility of the server-side data
10
10
  * driver which has access to the collection schema.
11
11
  *
12
- * Commas inside list values are backslash-escaped (`\,`), and literal
13
- * backslashes are escaped as `\\`.
12
+ * Structural characters inside a value are backslash-escaped: `,` `\,`,
13
+ * `(` `\(`, `)` → `\)`, and a literal backslash as `\\`. Decoding is
14
+ * deliberately conservative — only those four sequences are decoded, so a
15
+ * backslash that arrives unescaped from an older client survives intact.
14
16
  *
15
17
  * @module
16
18
  */
17
- import { FilterValues, LogicalCondition, FilterCondition } from "@rebasepro/types";
19
+ import { WhereFilterOp, FilterValues, LogicalCondition, FilterCondition } from "@rebasepro/types";
20
+ /**
21
+ * A filter condition named an operator this dialect does not have.
22
+ *
23
+ * ## Why this throws, rather than returning a typed rejection
24
+ *
25
+ * `deserializeFilter` is the *shared* codec: the REST ingress
26
+ * (`packages/server/src/api/rest/query-parser.ts`), the browser SDK and the
27
+ * admin panel (`buildRebaseData.ts`) all decode through it. Two constraints
28
+ * follow.
29
+ *
30
+ * - It cannot throw the server's `ApiError`. `@rebasepro/common` does not
31
+ * depend on `@rebasepro/server` (the dependency runs the other way), and a
32
+ * browser client has no error handler to render an `ApiError` with. So the
33
+ * rejection is this plain `Error` subclass, whose `message` reads correctly
34
+ * wherever it surfaces — a rejected promise in an app, a 400 body over HTTP.
35
+ * - It cannot be a returned rejection *value*. Every caller assigns the result
36
+ * straight into a query it is about to run; a sentinel that none of them
37
+ * check would be ignored, which is exactly the silently-wrong-filter failure
38
+ * this exists to stop. Throwing is also what this file already does for the
39
+ * sibling cases — `serializeTuple` on an unknown canonical operator,
40
+ * `deserializeLogicalCondition` past the nesting bound — and the REST parser
41
+ * already converts the latter into a 400.
42
+ *
43
+ * `statusCode`, `code` and `details` are carried as fields because the server's
44
+ * Hono error handler duck-types those off any thrown error: a decode path that
45
+ * forgets to convert still answers 400 with the canonical envelope instead of a
46
+ * 500 that says "An unexpected error occurred". `query-parser.ts` converts
47
+ * explicitly all the same — that is the path the contract is stated on, and an
48
+ * incidental 400 is not a contract.
49
+ */
50
+ export declare class UnknownFilterOperatorError extends Error {
51
+ /** The field the condition was written against. */
52
+ readonly field: string;
53
+ /** The operator string as it arrived, verbatim. */
54
+ readonly operator: string;
55
+ /** Every operator this dialect accepts, in canonical spelling. */
56
+ readonly validOperators: readonly WhereFilterOp[];
57
+ /** See the class docblock: read by the server's error handler. */
58
+ readonly statusCode = 400;
59
+ readonly code = "UNKNOWN_FILTER_OPERATOR";
60
+ readonly details: {
61
+ field: string;
62
+ operator: string;
63
+ validOperators: readonly WhereFilterOp[];
64
+ };
65
+ constructor(field: string, operator: string);
66
+ }
18
67
  /**
19
68
  * Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
20
69
  * querystring record.
@@ -48,6 +97,9 @@ export declare function serializeFilter(filter: FilterValues<string> | Record<st
48
97
  *
49
98
  * deserializeFilter({ age: ["gte.18", "lt.65"] })
50
99
  * // → { age: [[">=", "18"], ["<", "65"]] }
100
+ *
101
+ * @throws {UnknownFilterOperatorError} when a condition names an operator this
102
+ * dialect does not have. See that class for why a rejection here is a throw.
51
103
  */
52
104
  export declare function deserializeFilter(query: Record<string, unknown>): FilterValues<string>;
53
105
  /**
@@ -1,4 +1,4 @@
1
- import { CollectionAccessor, FilterCondition, FindResponse, LogicalCondition, QueryBuilderInterface, WhereFilterOp, WhereValue, type ComputedSortField } from "@rebasepro/types";
1
+ import { CollectionAccessor, FilterCondition, FindResponse, LogicalCondition, QueryBuilderInterface, WhereFilterOp, WhereValueFor, type ComputedSortField } from "@rebasepro/types";
2
2
  export declare function or(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition;
3
3
  export declare function and(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition;
4
4
  export declare function cond(column: string, operator: WhereFilterOp, value: unknown): FilterCondition;
@@ -11,7 +11,7 @@ export declare class QueryBuilder<M extends Record<string, unknown> = Record<str
11
11
  * @example
12
12
  * client.collection('users').where('age', '>=', 18).find()
13
13
  */
14
- where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
14
+ where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
15
15
  where(logicalCondition: LogicalCondition): this;
16
16
  /**
17
17
  * Order the results by a specific column.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./data/buildRoutedRebaseData";
5
5
  export * from "./data/resolveDataSource";
6
6
  export * from "./data/query_builder";
7
7
  export * from "./data/paginate";
8
+ export * from "./data/filter-conditions";
8
9
  export * from "./data/filter-dialect";
9
10
  export * from "./data/sort-dialect";
10
11
  export * from "./table-classification";