@rebasepro/common 0.13.0 → 0.13.1-canary.g1822133

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 (43) hide show
  1. package/dist/data/buildRebaseData.d.ts +10 -1
  2. package/dist/data/filter-conditions.d.ts +34 -0
  3. package/dist/data/filter-dialect.d.ts +70 -4
  4. package/dist/data/paginate.d.ts +20 -0
  5. package/dist/data/query_builder.d.ts +16 -4
  6. package/dist/data/resolveDataSource.d.ts +36 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.es.js +951 -123
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/util/auth-default-policies.d.ts +0 -8
  11. package/dist/util/builders.d.ts +2 -2
  12. package/dist/util/collections.d.ts +17 -0
  13. package/dist/util/conditions.d.ts +7 -3
  14. package/dist/util/entities.d.ts +8 -1
  15. package/dist/util/index.d.ts +1 -0
  16. package/dist/util/internal-tables.d.ts +95 -0
  17. package/dist/util/permissions.d.ts +30 -0
  18. package/dist/util/policy/sqlToPolicy.d.ts +4 -4
  19. package/dist/util/relations.d.ts +47 -1
  20. package/dist/util/resolutions.d.ts +31 -0
  21. package/package.json +5 -4
  22. package/src/data/buildRebaseData.ts +161 -27
  23. package/src/data/filter-conditions.ts +46 -0
  24. package/src/data/filter-dialect.ts +380 -52
  25. package/src/data/paginate.ts +30 -0
  26. package/src/data/query_builder.ts +26 -4
  27. package/src/data/resolveDataSource.ts +56 -0
  28. package/src/index.ts +1 -0
  29. package/src/util/auth-default-policies.ts +56 -15
  30. package/src/util/builders.ts +3 -3
  31. package/src/util/collections.ts +17 -1
  32. package/src/util/conditions.ts +8 -3
  33. package/src/util/entities.ts +15 -1
  34. package/src/util/index.ts +1 -0
  35. package/src/util/internal-tables.ts +154 -0
  36. package/src/util/permissions.test.ts +23 -2
  37. package/src/util/permissions.ts +43 -6
  38. package/src/util/pg-column-to-property.ts +26 -4
  39. package/src/util/policy/evaluatePolicy.ts +24 -2
  40. package/src/util/policy/policyToPostgres.ts +23 -10
  41. package/src/util/policy/sqlToPolicy.ts +180 -33
  42. package/src/util/relations.ts +75 -1
  43. package/src/util/resolutions.ts +77 -5
@@ -34,7 +34,16 @@ export declare function buildRebaseData(driver: DataDriver, options?: EntityData
34
34
  * admin `RebaseDataContext` — without it the admin renders rows with only their
35
35
  * `id`.
36
36
  */
37
- export declare function wrapAsEntityData(sdkData: RebaseSdkData, options?: EntityDataOptions): RebaseData;
37
+ /**
38
+ * Only the by-slug accessor is asked for, so only that is required.
39
+ *
40
+ * Taking a whole `RebaseSdkData` meant taking `RebaseSdkData<unknown>`, whose
41
+ * dynamic branch is an index signature — and no `RebaseSdkData<DB>` satisfies
42
+ * it, because its own `collection` method is not a `SDKCollectionClient`. So a
43
+ * caller holding a *typed* client could not pass it to a function that reads
44
+ * one method off it, and that method is identical on every instantiation.
45
+ */
46
+ export declare function wrapAsEntityData(sdkData: Pick<RebaseSdkData, "collection">, options?: EntityDataOptions): RebaseData;
38
47
  /**
39
48
  * Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
40
49
  *
@@ -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
  /**
@@ -72,4 +124,18 @@ export declare function serializeLogicalCondition(cond: LogicalCondition | Filte
72
124
  * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
73
125
  * // → { type: "or", conditions: [...] }
74
126
  */
75
- export declare function deserializeLogicalCondition(str: string): LogicalCondition | FilterCondition;
127
+ /**
128
+ * How deeply `or(...)`/`and(...)` groups may nest.
129
+ *
130
+ * This parser recurses once per level, on a value that arrives in a query
131
+ * string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
132
+ * stack size exceeded`, which a caller sees as a 500 about the call stack
133
+ * rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
134
+ * below that in practice, but "the HTTP layer happens to stop it" is not a
135
+ * bound this parser should rely on.
136
+ *
137
+ * Thirty-two is far past anything a real filter expresses; the deepest in this
138
+ * repository's own tests is three.
139
+ */
140
+ export declare const MAX_LOGICAL_NESTING_DEPTH = 32;
141
+ export declare function deserializeLogicalCondition(str: string, nesting?: number): LogicalCondition | FilterCondition;
@@ -45,6 +45,26 @@ export declare class RebasePaginationError extends Error {
45
45
  }
46
46
  /** The one thing a transport has to provide to be paginated. */
47
47
  export type PageFinder<M extends Record<string, unknown> = Record<string, unknown>> = (params: FindParams<M>) => Promise<FindResult<M>>;
48
+ /**
49
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
50
+ *
51
+ * Lives here, next to the walk, for the reason at the top of this file: every
52
+ * transport has to mean the same thing by "page two". Four of them did not —
53
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
54
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
55
+ * the published type documented a fourth number. Pages that overlap or skip
56
+ * rows are the mildest of those outcomes.
57
+ *
58
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
59
+ * is the value to hand a driver: it stays `undefined` when the caller named no
60
+ * offset, because keyset pagination seeks with a `where` clause and must not
61
+ * look like it is paging by offset.
62
+ */
63
+ export declare function resolveFindWindow(params?: Pick<FindParams, "limit" | "offset" | "page">): {
64
+ limit: number;
65
+ offset: number;
66
+ driverOffset: number | undefined;
67
+ };
48
68
  /**
49
69
  * Walk every row a query matches, yielding one row at a time and fetching the
50
70
  * next page only when the consumer asks for it.
@@ -1,4 +1,4 @@
1
- import { CollectionAccessor, FilterCondition, FindResponse, LogicalCondition, QueryBuilderInterface, WhereFilterOp, WhereValue } 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,14 +11,14 @@ 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.
18
18
  * @example
19
19
  * client.collection('users').orderBy('createdAt', 'desc').find()
20
20
  */
21
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
21
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
22
22
  /**
23
23
  * Limit the number of results returned.
24
24
  */
@@ -30,7 +30,19 @@ export declare class QueryBuilder<M extends Record<string, unknown> = Record<str
30
30
  /**
31
31
  * Set a free-text search string if supported by the backend.
32
32
  */
33
- search(searchString: string): this;
33
+ search(searchString: string, options?: {
34
+ explain?: boolean;
35
+ }): this;
36
+ /**
37
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
38
+ *
39
+ * Postgres only, over a property declared as `type: "vector"`. Rows come
40
+ * back with a `_distance`; `where` filters before the ordering.
41
+ */
42
+ vectorSearch(property: string, vector: number[], options?: {
43
+ distance?: "cosine" | "l2" | "inner_product";
44
+ threshold?: number;
45
+ }): this;
34
46
  /**
35
47
  * Include related entities in the response.
36
48
  * Relations will be populated with full entity data instead of just IDs.
@@ -41,3 +41,39 @@ export declare function createDataSourceRegistry(definitions?: DataSourceDefinit
41
41
  * @param registry optional registry of declared data sources
42
42
  */
43
43
  export declare function resolveDataSource(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): ResolvedDataSource;
44
+ /**
45
+ * Does a SQL toolchain own this collection's storage?
46
+ *
47
+ * "Owns the storage" means: something generates a table for it, pushes that
48
+ * table to a database, plans its RLS policies, and reports it as drifted when
49
+ * the two disagree. That is true of a Postgres collection and false of a
50
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
51
+ * migrates — and the two were never told apart. Every stage of the SQL
52
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
53
+ * collection declared next to the Postgres ones got a `pgTable` in the
54
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
55
+ * `db push` include list — where its name shielding a same-named real table
56
+ * from Atlas's exclude list is the one that can lose data.
57
+ *
58
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
59
+ * name check: an engine registered through `registerDataSourceCapabilities`
60
+ * gets the same treatment as the built-in ones.
61
+ *
62
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
63
+ * tooling (the CLI, the schema generator) has no data-source registry to
64
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
65
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
66
+ * including a collection generates a table nothing writes to; wrongly excluding
67
+ * one silently stops generating a table the app is serving from. Declare
68
+ * `engine` on a collection that is not SQL-backed and this is exact.
69
+ */
70
+ export declare function isRelationalCollection(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): boolean;
71
+ /**
72
+ * The subset of `collections` a SQL toolchain owns — see
73
+ * {@link isRelationalCollection}.
74
+ *
75
+ * Every stage that generates SQL from collections starts by calling this, so
76
+ * the rule lives in one place rather than being re-decided per generator. It
77
+ * keeps the input order.
78
+ */
79
+ export declare function relationalCollections<C extends DataSourceResolvable>(collections: readonly C[], registry?: DataSourceRegistry): C[];
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";