@rebasepro/client 0.14.0 → 0.14.1-canary.g7e666eb

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.
@@ -1,4 +1,4 @@
1
- import { FilterValues, FindResult, LogicalCondition, FilterCondition, OrderByTuple, WhereFilterOp } from "@rebasepro/types";
1
+ import { FilterValues, FindResult, LogicalCondition, FilterCondition, OrderBySpec, WhereFilterOp } from "@rebasepro/types";
2
2
  import { FindParams } from "./transport";
3
3
  /**
4
4
  * The server's page size when the caller does not ask for one.
@@ -39,7 +39,7 @@ export declare function matchesParams(row: Record<string, unknown>, params?: Fin
39
39
  * the row id as a tiebreak so paging through an unsorted-but-equal run does
40
40
  * not shuffle rows between pages.
41
41
  */
42
- export declare function sortRows<M extends Record<string, unknown>>(rows: M[], orderBy?: OrderByTuple): M[];
42
+ export declare function sortRows<M extends Record<string, unknown>>(rows: M[], orderBy?: OrderBySpec): M[];
43
43
  /**
44
44
  * Resolve `page`/`offset`/`limit` the way the server does.
45
45
  *
@@ -102,6 +102,6 @@ export declare function isExactlyEvaluable(params?: FindParams): boolean;
102
102
  * ascending, first descending) that matches Postgres and never reaches the
103
103
  * comparator.
104
104
  */
105
- export declare function isLocallySortable(rows: readonly Record<string, unknown>[], orderBy?: OrderByTuple): boolean;
105
+ export declare function isLocallySortable(rows: readonly Record<string, unknown>[], orderBy?: OrderBySpec): boolean;
106
106
  /** Run a full query — filter, sort, paginate — over a set of rows. */
107
107
  export declare function runLocalQuery<M extends Record<string, unknown>>(rows: M[], params?: FindParams): FindResult<M>;
@@ -25,6 +25,11 @@ export declare class SDKQueryBuilder<M extends Record<string, unknown> = Record<
25
25
  where(logicalCondition: LogicalCondition): this;
26
26
  /**
27
27
  * Order the results by a specific column.
28
+ *
29
+ * Call it again to add a tie-breaker rather than replace the sort: keys
30
+ * apply in the order they were added, so
31
+ * `.orderBy("roles").orderBy("created_at", "desc")` sorts by role and
32
+ * shows the newest first within each one.
28
33
  */
29
34
  orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
30
35
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/client",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.14.1-canary.g7e666eb",
5
5
  "description": "HTTP SDK client for the Rebase custom backend",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -29,9 +29,9 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@rebasepro/types": "0.14.0",
33
- "@rebasepro/common": "0.14.0",
34
- "@rebasepro/utils": "0.14.0"
32
+ "@rebasepro/utils": "0.14.1-canary.g7e666eb",
33
+ "@rebasepro/common": "0.14.1-canary.g7e666eb",
34
+ "@rebasepro/types": "0.14.1-canary.g7e666eb"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@jest/globals": "^30.4.1",
package/src/collection.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  WriteOptions,
13
13
  type ComputedSortField
14
14
  } from "@rebasepro/types";
15
- import { collectAllPages, paginateFind, resolveFindWindow } from "@rebasepro/common";
15
+ import { collectAllPages, normalizeOrderBy, paginateFind, resolveFindWindow } from "@rebasepro/common";
16
16
 
17
17
  import { SDKQueryBuilder } from "./sdk_query_builder";
18
18
 
@@ -429,8 +429,11 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
429
429
  // handed "20" where it expected a keyset value, and the
430
430
  // offset it does understand never arrived at all.
431
431
  offset: window.driverOffset,
432
- orderBy: params?.orderBy?.[0],
433
- order: params?.orderBy?.[1],
432
+ // The list form, so a multi-key sort reaches the socket
433
+ // whole. Indexing `[0]`/`[1]` here read a tuple-of-tuples as
434
+ // a field name and a direction, and a live subscription came
435
+ // back in a different order from the same query fetched.
436
+ orderBy: normalizeOrderBy(params?.orderBy),
434
437
  searchString: params?.searchString,
435
438
  searchExplain: params?.searchExplain
436
439
  },
@@ -4,12 +4,12 @@ import {
4
4
  FindResult,
5
5
  LogicalCondition,
6
6
  FilterCondition,
7
- OrderByTuple,
7
+ OrderBySpec,
8
8
  WhereFilterOp,
9
9
  toCanonicalOp
10
10
  } from "@rebasepro/types";
11
11
  import { FindParams } from "./transport";
12
- import { resolveFindWindow } from "@rebasepro/common";
12
+ import { normalizeOrderBy, resolveFindWindow } from "@rebasepro/common";
13
13
 
14
14
  /**
15
15
  * A local evaluator for `FindParams`, so cached rows can answer a query the
@@ -308,26 +308,43 @@ export function matchesParams(row: Record<string, unknown>, params?: FindParams)
308
308
  * the row id as a tiebreak so paging through an unsorted-but-equal run does
309
309
  * not shuffle rows between pages.
310
310
  */
311
- export function sortRows<M extends Record<string, unknown>>(rows: M[], orderBy?: OrderByTuple): M[] {
312
- if (!orderBy) return rows;
313
- const [field, direction = "asc"] = orderBy;
314
- const sign = direction === "desc" ? -1 : 1;
311
+ export function sortRows<M extends Record<string, unknown>>(rows: M[], orderBy?: OrderBySpec): M[] {
312
+ const keys = normalizeOrderBy(orderBy);
313
+ if (!keys) return rows;
315
314
  return rows.sort((a, b) => {
316
- const av = a[field];
317
- const bv = b[field];
318
- const aNull = isNullish(toComparable(av));
319
- const bNull = isNullish(toComparable(bv));
320
- if (aNull || bNull) {
321
- if (aNull && bNull) return tiebreak(a, b);
322
- // NULLS LAST ascending, NULLS FIRST descending.
323
- return (aNull ? 1 : -1) * (direction === "desc" ? -1 : 1);
315
+ for (const [field, direction = "asc"] of keys) {
316
+ const cmp = compareOnKey(a, b, field, direction);
317
+ // Equal on this key — and equal is not a decision, so the next key
318
+ // gets to make one. Returning the tiebreak here instead is what a
319
+ // single-key sort does at the end, and doing it per key would order
320
+ // by the id the moment two rows shared a role.
321
+ if (cmp !== 0) return cmp;
324
322
  }
325
- const cmp = compareValues(av, bv);
326
- if (cmp === undefined || cmp === 0) return tiebreak(a, b);
327
- return cmp * sign;
323
+ return tiebreak(a, b);
328
324
  });
329
325
  }
330
326
 
327
+ /** One key's verdict: negative, positive, or 0 for "these two are equal here". */
328
+ function compareOnKey(
329
+ a: Record<string, unknown>,
330
+ b: Record<string, unknown>,
331
+ field: string,
332
+ direction: "asc" | "desc"
333
+ ): number {
334
+ const av = a[field];
335
+ const bv = b[field];
336
+ const aNull = isNullish(toComparable(av));
337
+ const bNull = isNullish(toComparable(bv));
338
+ if (aNull || bNull) {
339
+ if (aNull && bNull) return 0;
340
+ // NULLS LAST ascending, NULLS FIRST descending.
341
+ return (aNull ? 1 : -1) * (direction === "desc" ? -1 : 1);
342
+ }
343
+ const cmp = compareValues(av, bv);
344
+ if (cmp === undefined || cmp === 0) return 0;
345
+ return cmp * (direction === "desc" ? -1 : 1);
346
+ }
347
+
331
348
  function tiebreak(a: Record<string, unknown>, b: Record<string, unknown>): number {
332
349
  const cmp = compareValues(a.id, b.id);
333
350
  return cmp ?? 0;
@@ -433,21 +450,22 @@ export function isExactlyEvaluable(params?: FindParams): boolean {
433
450
  */
434
451
  export function isLocallySortable(
435
452
  rows: readonly Record<string, unknown>[],
436
- orderBy?: OrderByTuple
453
+ orderBy?: OrderBySpec
437
454
  ): boolean {
438
- if (!orderBy) return true;
439
- const [field] = orderBy;
440
- for (const row of rows) {
455
+ const keys = normalizeOrderBy(orderBy);
456
+ if (!keys) return true;
457
+ // Every key has to be decidable, not just the first: a sort the local side
458
+ // can only agree with down to its second column is one it disagrees with.
459
+ return keys.every(([field]) => rows.every((row) => {
441
460
  const value = toComparable(row[field]);
442
- if (isNullish(value)) continue;
443
- if (typeof value === "number" || typeof value === "boolean") continue;
444
- if (typeof value === "bigint") continue;
461
+ if (isNullish(value)) return true;
462
+ if (typeof value === "number" || typeof value === "boolean") return true;
463
+ if (typeof value === "bigint") return true;
445
464
  // A numeric string is compared as a number, so it is safe too — this is
446
465
  // the wire's type erasure, which `compareValues` already undoes.
447
- if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) continue;
466
+ if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) return true;
448
467
  return false;
449
- }
450
- return true;
468
+ }));
451
469
  }
452
470
 
453
471
  /** Run a full query — filter, sort, paginate — over a set of rows. */
@@ -2,12 +2,14 @@ import {
2
2
  FindParams,
3
3
  FindResult,
4
4
  LogicalCondition,
5
+ OrderByTuple,
5
6
  SDKCollectionClient,
6
7
  SDKQueryBuilderInterface,
7
8
  WhereFilterOp,
8
9
  WhereValueFor,
9
10
  type ComputedSortField
10
11
  } from "@rebasepro/types";
12
+ import { normalizeOrderBy } from "@rebasepro/common";
11
13
 
12
14
  /**
13
15
  * SDK Query Builder — returns flat rows (`FindResult<M>`) instead of
@@ -72,9 +74,15 @@ export class SDKQueryBuilder<M extends Record<string, unknown> = Record<string,
72
74
 
73
75
  /**
74
76
  * Order the results by a specific column.
77
+ *
78
+ * Call it again to add a tie-breaker rather than replace the sort: keys
79
+ * apply in the order they were added, so
80
+ * `.orderBy("roles").orderBy("created_at", "desc")` sorts by role and
81
+ * shows the newest first within each one.
75
82
  */
76
83
  orderBy(column: (keyof M & string) | ComputedSortField, direction: "asc" | "desc" = "asc"): this {
77
- this.params.orderBy = [column, direction];
84
+ const existing = normalizeOrderBy(this.params.orderBy) ?? [];
85
+ this.params.orderBy = [...existing, [column, direction] as OrderByTuple];
78
86
  return this;
79
87
  }
80
88