@rebasepro/types 0.14.1 → 0.14.2-canary.g27a129e

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.
@@ -29,7 +29,7 @@
29
29
  *
30
30
  * @group Models
31
31
  */
32
- export declare const ADMIN_COLLECTION_KEYS: readonly ["Actions", "additionalFields", "alwaysApplyDefaultValues", "components", "defaultEntityAction", "defaultFilter", "defaultSelectedView", "defaultSize", "defaultViewMode", "disableDefaultActions", "display", "enabledViews", "entityActions", "entityViews", "exportable", "filterPresets", "fixedFilter", "form", "formAutoSave", "formView", "group", "hideFromEntityViews", "hideFromNavigation", "hideIdFromCollection", "hideIdFromForm", "icon", "includeJsonView", "inlineEditing", "kanban", "listProperties", "localChangesBackup", "openEntityMode", "orderProperty", "pagination", "previewProperties", "propertiesOrder", "selectionController", "selectionEnabled", "sideDialogWidth", "sort", "titleProperty"];
32
+ export declare const ADMIN_COLLECTION_KEYS: readonly ["Actions", "additionalFields", "alwaysApplyDefaultValues", "components", "customViews", "defaultEntityAction", "defaultFilter", "defaultSelectedView", "defaultSize", "defaultViewMode", "disableDefaultActions", "display", "enabledViews", "entityActions", "entityViews", "exportable", "filterPresets", "fixedFilter", "form", "formAutoSave", "formView", "group", "hideFromEntityViews", "hideFromNavigation", "hideIdFromCollection", "hideIdFromForm", "icon", "includeJsonView", "inlineEditing", "kanban", "listProperties", "localChangesBackup", "openEntityMode", "orderProperty", "pagination", "previewProperties", "propertiesOrder", "selectionController", "selectionEnabled", "sideDialogWidth", "sort", "titleProperty"];
33
33
  /** A key of a collection's `admin` block. @group Models */
34
34
  export type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];
35
35
  /**
@@ -68,6 +68,38 @@ export interface DataSourceCapabilities {
68
68
  * claimed rather than assumed: assuming them wrongly is the widening.
69
69
  */
70
70
  filterableRelationKinds?: readonly string[];
71
+ /**
72
+ * Can a filter address a *column of the related row* — `applications.status`
73
+ * — rather than only the related row's id?
74
+ *
75
+ * A separate capability from {@link filterableRelationKinds} because it is
76
+ * a separate subquery: the id filter stops at the junction, one of these
77
+ * reaches the target table and compares one of its columns. A driver can
78
+ * do the first and not the second.
79
+ *
80
+ * Optional and defaulting to **false**, for the reason the relation kinds
81
+ * default narrow: an unclaimed capability that the admin assumes is there
82
+ * produces a control whose query the driver answers by dropping the key —
83
+ * and a dropped filter key widens the read to every row.
84
+ *
85
+ * Meaningless without {@link supportsRelations}; a driver with no relations
86
+ * has nothing to reach through.
87
+ */
88
+ supportsRelationFieldFilters?: boolean;
89
+ /**
90
+ * Can a sort key be an aggregate over a to-many relation — "oldest waiting
91
+ * first", "busiest first"?
92
+ *
93
+ * Compiled as a correlated scalar subquery in `ORDER BY`, which a document
94
+ * store cannot express at all. Optional and defaulting to **false**.
95
+ *
96
+ * A wrongly claimed sort capability fails differently from a wrongly
97
+ * claimed filter one, and worse in one respect: a driver that cannot
98
+ * resolve the key drops the `ORDER BY` and answers 200 with rows in
99
+ * whatever order the database pleased, which reads as a sorted list. Paging
100
+ * over that repeats and skips rows.
101
+ */
102
+ relationAggregateSorts?: boolean;
71
103
  /** Does this source support SQL admin operations (SQL editor, EXPLAIN, etc.)? */
72
104
  supportsSQLAdmin: boolean;
73
105
  /** Does this source support document admin operations (aggregation, stats)? */
@@ -65,7 +65,110 @@ export type OrderByTuple<Key extends string = string> = [Key, "asc" | "desc"];
65
65
  *
66
66
  * @group Models
67
67
  */
68
- export type OrderBySpec<Key extends string = string> = OrderByTuple<Key> | OrderByTuple<Key>[];
68
+ export type OrderBySpec<Key extends string = string> = OrderBySortTuple<Key> | OrderBySortTuple<Key>[];
69
+ /**
70
+ * A sort key: a field name, or an aggregate over a to-many relation.
71
+ *
72
+ * @group Models
73
+ */
74
+ export type SortKey<Key extends string = string> = Key | RelationAggregateSort;
75
+ /**
76
+ * `[sortKey, direction]` — the authoring form of {@link OrderByTuple}, which
77
+ * additionally accepts a {@link RelationAggregateSort} object.
78
+ *
79
+ * The object never reaches a driver: `normalizeOrderBy` in `@rebasepro/common`
80
+ * encodes it to its string spelling on the way down, and everything below that
81
+ * point speaks plain `OrderByTuple`. See {@link RelationAggregateSort} for why
82
+ * the wire form is a string.
83
+ *
84
+ * @group Models
85
+ */
86
+ export type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, "asc" | "desc"];
87
+ /**
88
+ * The aggregate functions a relation sort can apply.
89
+ *
90
+ * Five, and no `array_agg`/`string_agg`: an aggregate used as a sort key has to
91
+ * produce something with an order, and these are the ones that do.
92
+ *
93
+ * @group Models
94
+ */
95
+ export type RelationAggregateFn = "min" | "max" | "count" | "sum" | "avg";
96
+ /**
97
+ * Order rows by an aggregate over the rows a to-many relation reaches —
98
+ * "candidates, oldest waiting first", "clients, busiest first".
99
+ *
100
+ * ```ts
101
+ * // The date of each candidate's earliest open application.
102
+ * orderBy: [[{ relation: "applications", field: "created_at", agg: "min" }, "asc"]]
103
+ *
104
+ * // How many applications each candidate has.
105
+ * orderBy: [[{ relation: "applications", agg: "count" }, "desc"]]
106
+ * ```
107
+ *
108
+ * This is the half of a queue that cannot be worked around client-side. A
109
+ * *filter* over a relation can be approximated by denormalising a flag onto the
110
+ * row; an *ordering* cannot be approximated at all once the result set is
111
+ * paged, because the client only ever holds one page and the page was chosen by
112
+ * the wrong order.
113
+ *
114
+ * Rows the relation reaches nothing from sort last ascending and first
115
+ * descending — the placement Postgres gives a `NULL`, stated rather than
116
+ * inherited, because the keyset comparison behind cursor paging has to agree
117
+ * with it exactly. Ties are broken by the row id, so the order is total and
118
+ * paging over it neither repeats nor skips.
119
+ *
120
+ * Compiled by the driver into a correlated subquery, so it is subject to the
121
+ * reader's own row-level security on the target table: a related row the reader
122
+ * cannot see does not contribute to the aggregate. Offered only where
123
+ * {@link DataSourceCapabilities.relationAggregateSorts} says the driver can
124
+ * compile it.
125
+ *
126
+ * @group Models
127
+ */
128
+ export interface RelationAggregateSort {
129
+ /** The to-many relation to aggregate over, by its name on this collection. */
130
+ relation: string;
131
+ /** The aggregate to apply. */
132
+ agg: RelationAggregateFn;
133
+ /**
134
+ * The column of the *target* to aggregate. Required by every function
135
+ * except `count`, which counts the related rows themselves when it is
136
+ * omitted — and counts the rows whose column is non-null when it is not.
137
+ */
138
+ field?: string;
139
+ }
140
+ /**
141
+ * A {@link RelationAggregateSort} as a single string — `min(applications.created_at)`,
142
+ * `count(applications)`.
143
+ *
144
+ * The wire form is a string because every layer below the call site already is
145
+ * one: `OrderByTuple` is `[string, direction]`, the REST parameter is
146
+ * `?orderBy=key:direction`, the driver contract takes `orderBy?: string |
147
+ * OrderByTuple[]`, and a cursor names its keys by string. `_score` established
148
+ * the same pattern — a sort key that is not a column, spelled as one — and this
149
+ * reuses it rather than widening five signatures to carry an object that would
150
+ * be flattened at the end anyway.
151
+ *
152
+ * SQL's own spelling, so the key reads as what it compiles to. Neither `:` nor
153
+ * `,` appears in it, which is what keeps it safe in the colon-delimited wire
154
+ * shorthand.
155
+ *
156
+ * @group Models
157
+ */
158
+ export declare function encodeRelationAggregateSort(sort: RelationAggregateSort): string;
159
+ /**
160
+ * Read the string spelling back, or `undefined` if it is not one.
161
+ *
162
+ * `undefined` rather than a throw: this is asked of *every* sort key to find
163
+ * out which kind it is, and an ordinary column name is not an error.
164
+ *
165
+ * @group Models
166
+ */
167
+ export declare function parseRelationAggregateSort(key: string): RelationAggregateSort | undefined;
168
+ /** Is this sort key the object form rather than a field name? */
169
+ export declare function isRelationAggregateSort(key: unknown): key is RelationAggregateSort;
170
+ /** A sort key in the single-string form every layer below the call site speaks. */
171
+ export declare function sortKeyToString(key: SortKey): string;
69
172
  /**
70
173
  * Canonical filter operators supported across all database backends.
71
174
  * Each DB driver translates these to its native query format.
@@ -103,22 +206,35 @@ export type WhereFilterOp = "<" | "<=" | "==" | "!=" | ">=" | ">" | "array-conta
103
206
  export type FilterValues<Key extends string> = Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;
104
207
  /**
105
208
  * The field names a query may address on a row type: every column, plus a
106
- * dotted path reaching inside one.
209
+ * dotted path reaching inside one — or *through a relation* to a column of the
210
+ * related row.
211
+ *
212
+ * A dotted path is not checked at all, in either direction. That is a
213
+ * deliberate loosening, and it is worth being exact about what it costs. The
214
+ * root used to be checked: `"meta.tag"` required a `meta` column. It cannot
215
+ * stay checked, because the other thing a dotted path now means is
216
+ * `"applications.status"` — and `applications` is a *relation*, which comes
217
+ * from the collection's `relations` and is not a column of `M` at all. There is
218
+ * nothing in a generated row type that could validate one. `FindParams.include`
219
+ * is `string[]` for exactly this reason and says so.
220
+ *
221
+ * So the guarantee moves rather than disappears: an unresolvable path is a 400
222
+ * from the driver, not a silently dropped condition. See
223
+ * `UnknownFilterFieldsMode` in `@rebasepro/server-postgres` — dropping a filter
224
+ * key *widens* the read to every row, which is why that resolution fails
225
+ * closed. A typo'd relation path is refused at runtime with the target
226
+ * collection's real column list in the message.
107
227
  *
108
- * Only the **root** of a dotted path is checked. `"meta.tag"` requires a `meta`
109
- * column and says nothing about what is under it, because what is under it is a
110
- * `map`/jsonb value whose shape the row type does not describe — and rejecting
111
- * paths we cannot verify would make jsonb columns unqueryable.
228
+ * Undotted keys are unaffected and still checked against `keyof M`.
112
229
  *
113
230
  * When `M` is left at its default `Record<string, unknown>`, `keyof M` is
114
- * `string` and a template literal over `string` is itself assignable to
115
- * `string`, so this collapses to `string` and every query stays permissive.
231
+ * `string` and this collapses to `string`, so every query stays permissive.
116
232
  * That is what keeps an untyped `createRebaseClient()` behaving exactly as it
117
233
  * did before the row type was threaded through.
118
234
  *
119
235
  * @group Models
120
236
  */
121
- export type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> = Extract<keyof M, string> | `${Extract<keyof M, string>}.${string}`;
237
+ export type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> = Extract<keyof M, string> | `${string}.${string}`;
122
238
  /**
123
239
  * Relaxed filter type that also accepts pre-serialized PostgREST strings.
124
240
  * **Internal only** — used at the wire-format boundary
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/types",
3
3
  "type": "module",
4
- "version": "0.14.1",
4
+ "version": "0.14.2-canary.g27a129e",
5
5
  "description": "Rebase type definitions — shared interfaces and controller types",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -35,6 +35,7 @@ export const ADMIN_COLLECTION_KEYS = [
35
35
  "additionalFields",
36
36
  "alwaysApplyDefaultValues",
37
37
  "components",
38
+ "customViews",
38
39
  "defaultEntityAction",
39
40
  "defaultFilter",
40
41
  "defaultSelectedView",
@@ -81,6 +81,40 @@ export interface DataSourceCapabilities {
81
81
  */
82
82
  filterableRelationKinds?: readonly string[];
83
83
 
84
+ /**
85
+ * Can a filter address a *column of the related row* — `applications.status`
86
+ * — rather than only the related row's id?
87
+ *
88
+ * A separate capability from {@link filterableRelationKinds} because it is
89
+ * a separate subquery: the id filter stops at the junction, one of these
90
+ * reaches the target table and compares one of its columns. A driver can
91
+ * do the first and not the second.
92
+ *
93
+ * Optional and defaulting to **false**, for the reason the relation kinds
94
+ * default narrow: an unclaimed capability that the admin assumes is there
95
+ * produces a control whose query the driver answers by dropping the key —
96
+ * and a dropped filter key widens the read to every row.
97
+ *
98
+ * Meaningless without {@link supportsRelations}; a driver with no relations
99
+ * has nothing to reach through.
100
+ */
101
+ supportsRelationFieldFilters?: boolean;
102
+
103
+ /**
104
+ * Can a sort key be an aggregate over a to-many relation — "oldest waiting
105
+ * first", "busiest first"?
106
+ *
107
+ * Compiled as a correlated scalar subquery in `ORDER BY`, which a document
108
+ * store cannot express at all. Optional and defaulting to **false**.
109
+ *
110
+ * A wrongly claimed sort capability fails differently from a wrongly
111
+ * claimed filter one, and worse in one respect: a driver that cannot
112
+ * resolve the key drops the `ORDER BY` and answers 200 with rows in
113
+ * whatever order the database pleased, which reads as a sorted list. Paging
114
+ * over that repeats and skips rows.
115
+ */
116
+ relationAggregateSorts?: boolean;
117
+
84
118
  // ── Admin capability flags ───────────────────────────────────────
85
119
  /** Does this source support SQL admin operations (SQL editor, EXPLAIN, etc.)? */
86
120
  supportsSQLAdmin: boolean;
@@ -213,6 +247,8 @@ export const POSTGRES_CAPABILITIES: DataSourceCapabilities = {
213
247
  // `via` is absent: its join path is authored source → target with no
214
248
  // stated inverse, so the driver has nothing to reverse into a filter.
215
249
  filterableRelationKinds: ["belongsTo", "manyToMany", "hasMany", "hasOne"],
250
+ supportsRelationFieldFilters: true,
251
+ relationAggregateSorts: true,
216
252
  supportsSQLAdmin: true,
217
253
  supportsDocumentAdmin: false,
218
254
  supportsSchemaAdmin: true
@@ -233,8 +269,11 @@ export const FIREBASE_CAPABILITIES: DataSourceCapabilities = {
233
269
  // family, so the UI must never offer it.
234
270
  filterOperators: ALL_WHERE_FILTER_OPS.filter(op =>
235
271
  op !== "like" && op !== "ilike" && op !== "not-like" && op !== "not-ilike"),
236
- // No relations at all — a document store links by reference.
272
+ // No relations at all — a document store links by reference. Nothing to
273
+ // reach through, so neither of the two relation-reaching features either.
237
274
  filterableRelationKinds: [],
275
+ supportsRelationFieldFilters: false,
276
+ relationAggregateSorts: false,
238
277
  supportsSQLAdmin: false,
239
278
  supportsDocumentAdmin: false,
240
279
  supportsSchemaAdmin: false
@@ -253,6 +292,8 @@ export const MONGODB_CAPABILITIES: DataSourceCapabilities = {
253
292
  supportsVectors: false,
254
293
  filterOperators: ALL_WHERE_FILTER_OPS,
255
294
  filterableRelationKinds: [],
295
+ supportsRelationFieldFilters: false,
296
+ relationAggregateSorts: false,
256
297
  supportsSQLAdmin: false,
257
298
  supportsDocumentAdmin: true,
258
299
  supportsSchemaAdmin: true
@@ -279,6 +320,12 @@ export const DEFAULT_CAPABILITIES: DataSourceCapabilities = {
279
320
  // whether a query is sent that an unknown driver may answer by dropping
280
321
  // the condition — which returns every row rather than none.
281
322
  filterableRelationKinds: DEFAULT_FILTERABLE_RELATION_KINDS,
323
+ // Narrow for the same reason, and more sharply. An unknown driver that is
324
+ // assumed to compile these answers by dropping the key: the filter widens
325
+ // the read to every row, and the sort comes back unordered while looking
326
+ // sorted. Both have to be claimed.
327
+ supportsRelationFieldFilters: false,
328
+ relationAggregateSorts: false,
282
329
  supportsSQLAdmin: true,
283
330
  supportsDocumentAdmin: true,
284
331
  supportsSchemaAdmin: true
@@ -67,7 +67,143 @@ export type OrderByTuple<Key extends string = string> = [Key, "asc" | "desc"];
67
67
  *
68
68
  * @group Models
69
69
  */
70
- export type OrderBySpec<Key extends string = string> = OrderByTuple<Key> | OrderByTuple<Key>[];
70
+ export type OrderBySpec<Key extends string = string> =
71
+ | OrderBySortTuple<Key>
72
+ | OrderBySortTuple<Key>[];
73
+
74
+ /**
75
+ * A sort key: a field name, or an aggregate over a to-many relation.
76
+ *
77
+ * @group Models
78
+ */
79
+ export type SortKey<Key extends string = string> = Key | RelationAggregateSort;
80
+
81
+ /**
82
+ * `[sortKey, direction]` — the authoring form of {@link OrderByTuple}, which
83
+ * additionally accepts a {@link RelationAggregateSort} object.
84
+ *
85
+ * The object never reaches a driver: `normalizeOrderBy` in `@rebasepro/common`
86
+ * encodes it to its string spelling on the way down, and everything below that
87
+ * point speaks plain `OrderByTuple`. See {@link RelationAggregateSort} for why
88
+ * the wire form is a string.
89
+ *
90
+ * @group Models
91
+ */
92
+ export type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, "asc" | "desc"];
93
+
94
+ /**
95
+ * The aggregate functions a relation sort can apply.
96
+ *
97
+ * Five, and no `array_agg`/`string_agg`: an aggregate used as a sort key has to
98
+ * produce something with an order, and these are the ones that do.
99
+ *
100
+ * @group Models
101
+ */
102
+ export type RelationAggregateFn = "min" | "max" | "count" | "sum" | "avg";
103
+
104
+ /**
105
+ * Order rows by an aggregate over the rows a to-many relation reaches —
106
+ * "candidates, oldest waiting first", "clients, busiest first".
107
+ *
108
+ * ```ts
109
+ * // The date of each candidate's earliest open application.
110
+ * orderBy: [[{ relation: "applications", field: "created_at", agg: "min" }, "asc"]]
111
+ *
112
+ * // How many applications each candidate has.
113
+ * orderBy: [[{ relation: "applications", agg: "count" }, "desc"]]
114
+ * ```
115
+ *
116
+ * This is the half of a queue that cannot be worked around client-side. A
117
+ * *filter* over a relation can be approximated by denormalising a flag onto the
118
+ * row; an *ordering* cannot be approximated at all once the result set is
119
+ * paged, because the client only ever holds one page and the page was chosen by
120
+ * the wrong order.
121
+ *
122
+ * Rows the relation reaches nothing from sort last ascending and first
123
+ * descending — the placement Postgres gives a `NULL`, stated rather than
124
+ * inherited, because the keyset comparison behind cursor paging has to agree
125
+ * with it exactly. Ties are broken by the row id, so the order is total and
126
+ * paging over it neither repeats nor skips.
127
+ *
128
+ * Compiled by the driver into a correlated subquery, so it is subject to the
129
+ * reader's own row-level security on the target table: a related row the reader
130
+ * cannot see does not contribute to the aggregate. Offered only where
131
+ * {@link DataSourceCapabilities.relationAggregateSorts} says the driver can
132
+ * compile it.
133
+ *
134
+ * @group Models
135
+ */
136
+ export interface RelationAggregateSort {
137
+ /** The to-many relation to aggregate over, by its name on this collection. */
138
+ relation: string;
139
+
140
+ /** The aggregate to apply. */
141
+ agg: RelationAggregateFn;
142
+
143
+ /**
144
+ * The column of the *target* to aggregate. Required by every function
145
+ * except `count`, which counts the related rows themselves when it is
146
+ * omitted — and counts the rows whose column is non-null when it is not.
147
+ */
148
+ field?: string;
149
+ }
150
+
151
+ /** The wire spelling of a {@link RelationAggregateSort}: `min(applications.created_at)`. */
152
+ const RELATION_AGGREGATE_SORT_PATTERN = /^(min|max|count|sum|avg)\(([^().]+)(?:\.([^()]+))?\)$/;
153
+
154
+ /**
155
+ * A {@link RelationAggregateSort} as a single string — `min(applications.created_at)`,
156
+ * `count(applications)`.
157
+ *
158
+ * The wire form is a string because every layer below the call site already is
159
+ * one: `OrderByTuple` is `[string, direction]`, the REST parameter is
160
+ * `?orderBy=key:direction`, the driver contract takes `orderBy?: string |
161
+ * OrderByTuple[]`, and a cursor names its keys by string. `_score` established
162
+ * the same pattern — a sort key that is not a column, spelled as one — and this
163
+ * reuses it rather than widening five signatures to carry an object that would
164
+ * be flattened at the end anyway.
165
+ *
166
+ * SQL's own spelling, so the key reads as what it compiles to. Neither `:` nor
167
+ * `,` appears in it, which is what keeps it safe in the colon-delimited wire
168
+ * shorthand.
169
+ *
170
+ * @group Models
171
+ */
172
+ export function encodeRelationAggregateSort(sort: RelationAggregateSort): string {
173
+ return `${sort.agg}(${sort.relation}${sort.field ? `.${sort.field}` : ""})`;
174
+ }
175
+
176
+ /**
177
+ * Read the string spelling back, or `undefined` if it is not one.
178
+ *
179
+ * `undefined` rather than a throw: this is asked of *every* sort key to find
180
+ * out which kind it is, and an ordinary column name is not an error.
181
+ *
182
+ * @group Models
183
+ */
184
+ export function parseRelationAggregateSort(key: string): RelationAggregateSort | undefined {
185
+ const match = RELATION_AGGREGATE_SORT_PATTERN.exec(key);
186
+ if (!match) return undefined;
187
+ const [, agg, relation, field] = match;
188
+ // `min()` and friends have nothing to aggregate without a column, and a
189
+ // key that parses to a half-built sort would resolve to no expression and
190
+ // be dropped — leaving the rows unsorted while the caller believes
191
+ // otherwise. `count` is the one function that means something on its own.
192
+ if (!field && agg !== "count") return undefined;
193
+ return { agg: agg as RelationAggregateFn, relation, ...(field && { field }) };
194
+ }
195
+
196
+ /** Is this sort key the object form rather than a field name? */
197
+ export function isRelationAggregateSort(key: unknown): key is RelationAggregateSort {
198
+ return typeof key === "object" && key !== null &&
199
+ typeof (key as RelationAggregateSort).relation === "string" &&
200
+ typeof (key as RelationAggregateSort).agg === "string";
201
+ }
202
+
203
+ /** A sort key in the single-string form every layer below the call site speaks. */
204
+ export function sortKeyToString(key: SortKey): string {
205
+ return isRelationAggregateSort(key) ? encodeRelationAggregateSort(key) : key;
206
+ }
71
207
 
72
208
  /**
73
209
  * Canonical filter operators supported across all database backends.
@@ -125,16 +261,29 @@ export type FilterValues<Key extends string> =
125
261
 
126
262
  /**
127
263
  * The field names a query may address on a row type: every column, plus a
128
- * dotted path reaching inside one.
264
+ * dotted path reaching inside one — or *through a relation* to a column of the
265
+ * related row.
266
+ *
267
+ * A dotted path is not checked at all, in either direction. That is a
268
+ * deliberate loosening, and it is worth being exact about what it costs. The
269
+ * root used to be checked: `"meta.tag"` required a `meta` column. It cannot
270
+ * stay checked, because the other thing a dotted path now means is
271
+ * `"applications.status"` — and `applications` is a *relation*, which comes
272
+ * from the collection's `relations` and is not a column of `M` at all. There is
273
+ * nothing in a generated row type that could validate one. `FindParams.include`
274
+ * is `string[]` for exactly this reason and says so.
275
+ *
276
+ * So the guarantee moves rather than disappears: an unresolvable path is a 400
277
+ * from the driver, not a silently dropped condition. See
278
+ * `UnknownFilterFieldsMode` in `@rebasepro/server-postgres` — dropping a filter
279
+ * key *widens* the read to every row, which is why that resolution fails
280
+ * closed. A typo'd relation path is refused at runtime with the target
281
+ * collection's real column list in the message.
129
282
  *
130
- * Only the **root** of a dotted path is checked. `"meta.tag"` requires a `meta`
131
- * column and says nothing about what is under it, because what is under it is a
132
- * `map`/jsonb value whose shape the row type does not describe — and rejecting
133
- * paths we cannot verify would make jsonb columns unqueryable.
283
+ * Undotted keys are unaffected and still checked against `keyof M`.
134
284
  *
135
285
  * When `M` is left at its default `Record<string, unknown>`, `keyof M` is
136
- * `string` and a template literal over `string` is itself assignable to
137
- * `string`, so this collapses to `string` and every query stays permissive.
286
+ * `string` and this collapses to `string`, so every query stays permissive.
138
287
  * That is what keeps an untyped `createRebaseClient()` behaving exactly as it
139
288
  * did before the row type was threaded through.
140
289
  *
@@ -142,7 +291,7 @@ export type FilterValues<Key extends string> =
142
291
  */
143
292
  export type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> =
144
293
  | Extract<keyof M, string>
145
- | `${Extract<keyof M, string>}.${string}`;
294
+ | `${string}.${string}`;
146
295
 
147
296
  /**
148
297
  * Relaxed filter type that also accepts pre-serialized PostgREST strings.