@rebasepro/common 0.14.2-canary.gca521e9 → 0.15.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/common",
3
3
  "type": "module",
4
- "version": "0.14.2-canary.gca521e9",
4
+ "version": "0.15.0",
5
5
  "description": "Rebase shared core — collection registry, data driver adapter and fluent query builder. No React dependency.",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "fast-equals": "6.0.2",
42
42
  "json-logic-js": "^2.0.5",
43
- "@rebasepro/types": "0.14.2-canary.gca521e9",
44
- "@rebasepro/utils": "0.14.2-canary.gca521e9"
43
+ "@rebasepro/types": "0.15.0",
44
+ "@rebasepro/utils": "0.15.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@jest/globals": "^30.4.1",
@@ -1,4 +1,5 @@
1
- import type { OrderBySpec, OrderByTuple } from "@rebasepro/types";
1
+ import type { OrderBySortTuple, OrderBySpec, OrderByTuple } from "@rebasepro/types";
2
+ import { isRelationAggregateSort, sortKeyToString } from "@rebasepro/types";
2
3
 
3
4
  /**
4
5
  * Sort-order wire codec.
@@ -20,16 +21,27 @@ import type { OrderBySpec, OrderByTuple } from "@rebasepro/types";
20
21
  * the same value; the two are told apart by whether the first element is
21
22
  * itself an array, which no field name ever is.
22
23
  *
24
+ * This is also where a {@link RelationAggregateSort} object stops being an
25
+ * object. Above this function a sort key may be either spelling; below it,
26
+ * every key is a string — which is what `OrderByTuple`, the REST parameter, the
27
+ * driver contract and the cursor all already were. Doing it here means the one
28
+ * place that already collapses the two *shapes* of a sort also collapses the
29
+ * two *spellings* of a key, rather than every consumer learning about both.
30
+ *
23
31
  * @returns The keys in order of significance, or `undefined` for no sort. An
24
32
  * empty list also returns `undefined` — "sort by nothing" is no sort, and
25
33
  * letting `[]` through would have every layer below re-deciding what it meant.
26
34
  */
27
35
  export function normalizeOrderBy(orderBy?: OrderBySpec): OrderByTuple[] | undefined {
28
36
  if (!orderBy || orderBy.length === 0) return undefined;
37
+ // An aggregate key is an object, so the first element being an array still
38
+ // tells the list form from the single-tuple one — no field name is an
39
+ // array, and neither is an aggregate key.
29
40
  const list = Array.isArray(orderBy[0])
30
- ? orderBy as OrderByTuple[]
31
- : [orderBy as OrderByTuple];
32
- return list.length > 0 ? list : undefined;
41
+ ? orderBy as OrderBySortTuple[]
42
+ : [orderBy as OrderBySortTuple];
43
+ if (list.length === 0) return undefined;
44
+ return list.map(([key, direction]) => [sortKeyToString(key), direction] as OrderByTuple);
33
45
  }
34
46
 
35
47
  /**
@@ -99,21 +111,30 @@ export function parseOrderBySpecStrict(raw: unknown, order?: "asc" | "desc"): Or
99
111
  throw new OrderBySpecError(`${typeof raw} is not a field name or a list of sort keys`);
100
112
  }
101
113
 
102
- // The single-tuple spelling, `["created_at", "desc"]`.
103
- if (typeof raw[0] === "string") return [toStrictTuple(raw, 0)];
114
+ // The single-tuple spelling, `["created_at", "desc"]` — or the same shape
115
+ // with an aggregate key in place of the field name.
116
+ if (typeof raw[0] === "string" || isRelationAggregateSort(raw[0])) return [toStrictTuple(raw, 0)];
104
117
 
105
118
  return raw.map(toStrictTuple);
106
119
  }
107
120
 
108
121
  function toStrictTuple(raw: unknown, index: number): OrderByTuple {
109
- if (!Array.isArray(raw) || typeof raw[0] !== "string" || raw[0].trim() === "") {
122
+ if (!Array.isArray(raw)) {
123
+ throw new OrderBySpecError(`entry ${index} has no field name`);
124
+ }
125
+ // The object spelling of an aggregate key, from an untyped caller that did
126
+ // not go through `normalizeOrderBy`. Encoded rather than refused: it is a
127
+ // sort this understands, and rejecting the shape a typed caller writes
128
+ // would be a distinction between the two spellings that nothing else makes.
129
+ const key = isRelationAggregateSort(raw[0]) ? sortKeyToString(raw[0]) : raw[0];
130
+ if (typeof key !== "string" || key.trim() === "") {
110
131
  throw new OrderBySpecError(`entry ${index} has no field name`);
111
132
  }
112
133
  const direction = raw[1];
113
134
  if (direction !== undefined && direction !== "asc" && direction !== "desc") {
114
135
  throw new OrderBySpecError(`entry ${index} has direction '${String(direction)}'`);
115
136
  }
116
- return [raw[0], direction ?? "asc"];
137
+ return [key, direction ?? "asc"];
117
138
  }
118
139
 
119
140
  /**
@@ -143,6 +164,9 @@ export function serializeOrderBy(orderBy?: OrderBySpec | string): string | undef
143
164
  if (!orderBy) return undefined;
144
165
  // Runtime tolerance: pass through a pre-serialized wire string unchanged.
145
166
  if (typeof orderBy === "string") return orderBy;
167
+ // `normalizeOrderBy` has already encoded any aggregate key to its string
168
+ // spelling, which is why the shorthand below can assume a string: neither
169
+ // `min(applications.created_at)` nor `count(applications)` contains a `:`.
146
170
  const list = normalizeOrderBy(orderBy);
147
171
  if (!list) return undefined;
148
172
  if (list.length === 1) return `${list[0][0]}:${list[0][1]}`;
@@ -80,6 +80,7 @@ export const REBASE_INTERNAL_TABLES: readonly string[] = [
80
80
  "api_keys",
81
81
  "cron_logs",
82
82
  "cron_claims",
83
+ "rate_limit_hits",
83
84
  "idempotency_keys",
84
85
  "entity_history",
85
86
  "branches",