@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.
- package/dist/index.es.js +60 -1
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +1 -1
- package/dist/types/data_source.d.ts +32 -0
- package/dist/types/filter-operators.d.ts +125 -9
- package/package.json +1 -1
- package/src/types/admin_block.ts +1 -0
- package/src/types/data_source.ts +48 -1
- package/src/types/filter-operators.ts +158 -9
|
@@ -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> =
|
|
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
|
-
*
|
|
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
|
|
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> | `${
|
|
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
package/src/types/admin_block.ts
CHANGED
package/src/types/data_source.ts
CHANGED
|
@@ -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> =
|
|
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
|
-
*
|
|
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
|
|
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
|
-
| `${
|
|
294
|
+
| `${string}.${string}`;
|
|
146
295
|
|
|
147
296
|
/**
|
|
148
297
|
* Relaxed filter type that also accepts pre-serialized PostgREST strings.
|