@rebasepro/client 0.8.0 → 0.9.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.
@@ -60,7 +60,7 @@ describe("createCollectionClient", () => {
60
60
 
61
61
  const client = createCollectionClient(transport, "items");
62
62
  const result = await client.count({
63
- orderBy: "created_at:desc"
63
+ orderBy: ["created_at", "desc"]
64
64
  });
65
65
 
66
66
  const calledUrl = (transport.request as ReturnType<typeof jest.fn>).mock.calls[0][0] as string;
@@ -123,7 +123,7 @@ offset: 10 });
123
123
  });
124
124
 
125
125
  describe("find()", () => {
126
- it("should call the list endpoint and return entities", async () => {
126
+ it("should call the list endpoint and return flat rows", async () => {
127
127
  (transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({
128
128
  data: [{ id: "1",
129
129
  name: "Product A" }],
@@ -141,12 +141,101 @@ hasMore: false }
141
141
  { method: "GET" }
142
142
  );
143
143
  expect(result.data).toHaveLength(1);
144
+ // Flat row access — no .values wrapper
144
145
  expect(result.data[0].id).toBe("1");
145
- expect(result.data[0].path).toBe("products");
146
+ expect((result.data[0] as Record<string, unknown>).name).toBe("Product A");
147
+ // No path field — that's CMS leakage
148
+ expect((result.data[0] as Record<string, unknown>).path).toBeUndefined();
146
149
  expect(result.meta.total).toBe(1);
147
150
  });
148
151
  });
149
152
 
153
+ describe("findById()", () => {
154
+ it("should return a flat row directly", async () => {
155
+ (transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ id: "42", title: "Hello" });
156
+
157
+ const client = createCollectionClient(transport, "posts");
158
+ const result = await client.findById("42");
159
+
160
+ expect(transport.request).toHaveBeenCalledWith(
161
+ "/data/posts/42",
162
+ { method: "GET" }
163
+ );
164
+ expect(result).toBeDefined();
165
+ expect(result!.id).toBe("42");
166
+ expect((result as Record<string, unknown>).title).toBe("Hello");
167
+ });
168
+
169
+ it("should return undefined for 404", async () => {
170
+ const { RebaseApiError } = await import("./transport");
171
+ (transport.request as ReturnType<typeof jest.fn>).mockRejectedValue(
172
+ new RebaseApiError("Not Found", { status: 404 })
173
+ );
174
+
175
+ const client = createCollectionClient(transport, "posts");
176
+ const result = await client.findById("999");
177
+ expect(result).toBeUndefined();
178
+ });
179
+ });
180
+
181
+ describe("create()", () => {
182
+ it("should return a flat row", async () => {
183
+ (transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ id: "new-1", title: "Created" });
184
+
185
+ const client = createCollectionClient(transport, "posts");
186
+ const result = await client.create({ title: "Created" } as Record<string, unknown>);
187
+
188
+ expect(result.id).toBe("new-1");
189
+ expect((result as Record<string, unknown>).title).toBe("Created");
190
+ });
191
+ });
192
+
193
+ describe("update()", () => {
194
+ it("should return a flat row", async () => {
195
+ (transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ id: "1", title: "Updated" });
196
+
197
+ const client = createCollectionClient(transport, "posts");
198
+ const result = await client.update("1", { title: "Updated" } as Record<string, unknown>);
199
+
200
+ expect(result.id).toBe("1");
201
+ expect((result as Record<string, unknown>).title).toBe("Updated");
202
+ });
203
+
204
+ it("should throw RebaseApiError for 404", async () => {
205
+ const { RebaseApiError } = await import("./transport");
206
+ (transport.request as ReturnType<typeof jest.fn>).mockRejectedValue(
207
+ new RebaseApiError("Not Found", { status: 404 })
208
+ );
209
+
210
+ const client = createCollectionClient(transport, "posts");
211
+ await expect(
212
+ client.update("999", { title: "Nope" } as Record<string, unknown>)
213
+ ).rejects.toMatchObject({ status: 404 });
214
+ });
215
+ });
216
+
217
+ describe("delete()", () => {
218
+ it("should throw RebaseApiError for 404", async () => {
219
+ const { RebaseApiError } = await import("./transport");
220
+ (transport.request as ReturnType<typeof jest.fn>).mockRejectedValue(
221
+ new RebaseApiError("Not Found", { status: 404 })
222
+ );
223
+
224
+ const client = createCollectionClient(transport, "posts");
225
+ await expect(client.delete("999")).rejects.toMatchObject({ status: 404 });
226
+ });
227
+
228
+ it("should propagate non-404 errors", async () => {
229
+ const { RebaseApiError } = await import("./transport");
230
+ (transport.request as ReturnType<typeof jest.fn>).mockRejectedValue(
231
+ new RebaseApiError("Internal Server Error", { status: 500 })
232
+ );
233
+
234
+ const client = createCollectionClient(transport, "posts");
235
+ await expect(client.delete("1")).rejects.toThrow("Internal Server Error");
236
+ });
237
+ });
238
+
150
239
  describe("count() is defined", () => {
151
240
  it("should have count as a defined function on the accessor", () => {
152
241
  const client = createCollectionClient(transport, "products");
package/src/collection.ts CHANGED
@@ -1,52 +1,31 @@
1
1
  import { buildQueryString, FindParams, RebaseApiError, Transport } from "./transport";
2
2
  import { RebaseWebSocketClient } from "./websocket";
3
3
  import {
4
- CollectionAccessor,
5
- Entity,
6
- FilterValues,
7
- FindResponse,
4
+ FindResult,
8
5
  LogicalCondition,
6
+ SDKCollectionClient,
7
+ SDKQueryBuilderInterface,
9
8
  WhereFilterOp,
10
9
  WhereValue
11
10
  } from "@rebasepro/types";
12
11
 
13
- import { QueryBuilder } from "./query_builder";
14
-
15
-
16
- /**
17
- * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
18
- * a proper `Entity<M>` structure expected by the core framework.
19
- * The `id` is kept inside `values` as well, since collection properties
20
- * may define an `isId` field that the form binds to `formex.values`.
21
- */
22
- function rowToEntity<M extends Record<string, unknown>>(row: Record<string, unknown>, slug: string): Entity<M> {
23
- return {
24
- id: row.id as string | number,
25
- path: slug,
26
- values: row as M
27
- };
28
- }
12
+ import { SDKQueryBuilder } from "./sdk_query_builder";
29
13
 
30
14
  /**
31
- * CollectionClient extends `CollectionAccessor` from `@rebasepro/types` so that
32
- * `client.data` can be passed directly to the core Rebase component.
15
+ * The concrete, HTTP-backed implementation of the public
16
+ * {@link SDKCollectionClient} contract flat rows (no Entity wrapper), plus
17
+ * fluent query-builder methods (`.where()`, `.orderBy()`, …).
33
18
  *
34
- * Additionally it exposes fluent query builder methods like `.where()`, `.orderBy()`.
19
+ * This is what `createRebaseClient().data.<collection>` returns. It is not a
20
+ * separate API from {@link SDKCollectionClient}; it only widens it with
21
+ * `count()`. Program against {@link SDKCollectionClient} when you want a
22
+ * transport-agnostic type.
35
23
  */
36
- export interface CollectionClient<M extends Record<string, unknown> = Record<string, unknown>> extends CollectionAccessor<M> {
37
- where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilder<M>;
38
- where(logicalCondition: LogicalCondition): QueryBuilder<M>;
39
-
40
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilder<M>;
41
-
42
- limit(count: number): QueryBuilder<M>;
43
-
44
- offset(count: number): QueryBuilder<M>;
45
-
46
- search(searchString: string): QueryBuilder<M>;
47
-
48
- include(...relations: string[]): QueryBuilder<M>;
49
-
24
+ export interface CollectionClient<
25
+ M extends Record<string, unknown> = Record<string, unknown>,
26
+ I = Partial<M>,
27
+ U = Partial<M>
28
+ > extends SDKCollectionClient<M, I, U> {
50
29
  count(params?: FindParams): Promise<number>;
51
30
  }
52
31
 
@@ -54,14 +33,14 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
54
33
  const basePath = `/data/${slug}`;
55
34
 
56
35
  const client: CollectionClient<M> = {
57
- async find(params?: FindParams): Promise<FindResponse<M>> {
36
+ async find(params?: FindParams): Promise<FindResult<M>> {
58
37
  const qs = buildQueryString(params);
59
38
  const raw = await transport.request<{
60
39
  data: Record<string, unknown>[];
61
- meta: FindResponse<M>["meta"]
40
+ meta: FindResult<M>["meta"]
62
41
  }>(basePath + qs, { method: "GET" });
63
42
  return {
64
- data: (raw.data || []).map((row: Record<string, unknown>) => rowToEntity<M>(row, slug)),
43
+ data: (raw.data || []) as M[],
65
44
  meta: raw.meta
66
45
  };
67
46
  },
@@ -70,7 +49,7 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
70
49
  try {
71
50
  const raw = await transport.request<Record<string, unknown>>(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
72
51
  if (!raw) return undefined;
73
- return rowToEntity<M>(raw, slug);
52
+ return raw as M;
74
53
  } catch (err) {
75
54
  if (err instanceof RebaseApiError && err.status === 404) {
76
55
  return undefined;
@@ -88,7 +67,7 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
88
67
  method: "POST",
89
68
  body: JSON.stringify(body)
90
69
  });
91
- return rowToEntity<M>(raw, slug);
70
+ return raw as M;
92
71
  },
93
72
 
94
73
  async update(id: string | number, data: Partial<M>) {
@@ -96,11 +75,11 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
96
75
  method: "PUT",
97
76
  body: JSON.stringify(data)
98
77
  });
99
- return rowToEntity<M>(raw, slug);
78
+ return raw as M;
100
79
  },
101
80
 
102
81
  async delete(id: string | number) {
103
- return transport.request<void>(`${basePath}/${encodeURIComponent(String(id))}`, {
82
+ await transport.request<void>(`${basePath}/${encodeURIComponent(String(id))}`, {
104
83
  method: "DELETE"
105
84
  });
106
85
  },
@@ -118,31 +97,31 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
118
97
 
119
98
  // Fluent builder instantiation
120
99
  where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown) {
121
- const builder = new QueryBuilder<M>(client as unknown as CollectionAccessor<M>);
100
+ const builder = new SDKQueryBuilder<M>(client);
122
101
  if (typeof columnOrCondition === "object") {
123
102
  return builder.where(columnOrCondition);
124
103
  }
125
104
  return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
126
105
  },
127
106
  orderBy(column: keyof M & string, direction?: "asc" | "desc") {
128
- return new QueryBuilder<M>(client).orderBy(column, direction);
107
+ return new SDKQueryBuilder<M>(client).orderBy(column, direction);
129
108
  },
130
109
  limit(count: number) {
131
- return new QueryBuilder<M>(client).limit(count);
110
+ return new SDKQueryBuilder<M>(client).limit(count);
132
111
  },
133
112
  offset(count: number) {
134
- return new QueryBuilder<M>(client).offset(count);
113
+ return new SDKQueryBuilder<M>(client).offset(count);
135
114
  },
136
115
  search(searchString: string) {
137
- return new QueryBuilder<M>(client).search(searchString);
116
+ return new SDKQueryBuilder<M>(client).search(searchString);
138
117
  },
139
118
  include(...relations: string[]) {
140
- return new QueryBuilder<M>(client).include(...relations);
119
+ return new SDKQueryBuilder<M>(client).include(...relations);
141
120
  }
142
121
  };
143
122
 
144
123
  if (ws) {
145
- client.listen = (params: FindParams | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void) => {
124
+ client.listen = (params: FindParams | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void) => {
146
125
  let active = true;
147
126
  let lastUpdateId = 0;
148
127
  const unsub = ws.listenCollection(
@@ -151,45 +130,63 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
151
130
  filter: params?.where,
152
131
  limit: params?.limit,
153
132
  startAfter: params?.offset ? String(params.offset) : undefined,
154
- orderBy: params?.orderBy?.split(":")[0],
155
- order: params?.orderBy?.split(":")[1] as "asc" | "desc",
133
+ orderBy: params?.orderBy?.[0],
134
+ order: params?.orderBy?.[1],
156
135
  searchString: params?.searchString
157
136
  },
158
- (entities: Entity[]) => {
137
+ (incomingRows: Record<string, unknown>[]) => {
159
138
  const currentUpdateId = ++lastUpdateId;
160
139
  const requestedLimit = params?.limit || 20;
161
140
  const offset = params?.offset || 0;
162
141
 
163
- // Immediately fire update with heuristic metadata
164
- onUpdate({
165
- data: entities as Entity<M>[],
166
- meta: {
167
- total: entities.length,
168
- limit: requestedLimit,
169
- offset,
170
- hasMore: entities.length >= requestedLimit
171
- }
172
- });
173
-
174
- // Asynchronously fetch the actual count from the server to get accurate total/hasMore
142
+ // WS client already delivers flat rows — just cast
143
+ const rows = incomingRows as M[];
144
+
145
+ // Heuristic metadata (used as fallback if count call fails)
146
+ const heuristicTotal = rows.length;
147
+ const heuristicHasMore = rows.length >= requestedLimit;
148
+
149
+ // Try to get authoritative count; fall back to heuristic
175
150
  if (client.count) {
176
151
  client.count(params)
177
152
  .then((total) => {
178
153
  if (active && currentUpdateId === lastUpdateId) {
179
154
  onUpdate({
180
- data: entities as Entity<M>[],
155
+ data: rows,
181
156
  meta: {
182
157
  total,
183
158
  limit: requestedLimit,
184
159
  offset,
185
- hasMore: offset + entities.length < total
160
+ hasMore: offset + rows.length < total
186
161
  }
187
162
  });
188
163
  }
189
164
  })
190
165
  .catch(() => {
191
- // Silent fallback on count error
166
+ // Count failed use heuristic meta
167
+ if (active && currentUpdateId === lastUpdateId) {
168
+ onUpdate({
169
+ data: rows,
170
+ meta: {
171
+ total: heuristicTotal,
172
+ limit: requestedLimit,
173
+ offset,
174
+ hasMore: heuristicHasMore
175
+ }
176
+ });
177
+ }
192
178
  });
179
+ } else {
180
+ // No count method — fire immediately with heuristic meta
181
+ onUpdate({
182
+ data: rows,
183
+ meta: {
184
+ total: heuristicTotal,
185
+ limit: requestedLimit,
186
+ offset,
187
+ hasMore: heuristicHasMore
188
+ }
189
+ });
193
190
  }
194
191
  },
195
192
  onError
@@ -201,15 +198,15 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
201
198
  };
202
199
  };
203
200
 
204
- client.listenById = (id: string | number, onUpdate: (data: Entity<M> | undefined) => void, onError?: (error: Error) => void) => {
205
- return ws.listenEntity(
201
+ client.listenById = (id: string | number, onUpdate: (data: M | undefined) => void, onError?: (error: Error) => void) => {
202
+ return ws.listenOne(
206
203
  {
207
204
  path: slug,
208
- entityId: String(id)
205
+ id: String(id)
209
206
  },
210
- (entity: Entity | null) => {
211
- if (entity) {
212
- onUpdate(entity as Entity<M>);
207
+ (row: Record<string, unknown> | null) => {
208
+ if (row) {
209
+ onUpdate(row as M);
213
210
  } else {
214
211
  onUpdate(undefined);
215
212
  }
@@ -0,0 +1,167 @@
1
+ import { jest } from "@jest/globals";
2
+ import { createRebaseClient, RebaseClientError } from "./index";
3
+
4
+ // Minimal mock: we only need the proxy behavior, not real HTTP calls.
5
+ // The global fetch mock prevents the transport from making real requests.
6
+ const originalFetch = globalThis.fetch;
7
+ beforeAll(() => {
8
+ globalThis.fetch = jest.fn<typeof fetch>().mockResolvedValue(new Response("{}")) as typeof fetch;
9
+ });
10
+ afterAll(() => {
11
+ globalThis.fetch = originalFetch;
12
+ });
13
+
14
+ const COLLECTIONS = {
15
+ products: "products",
16
+ orders: "orders",
17
+ blogPosts: "blog-posts",
18
+ companyMembers: "company_members",
19
+ } as const;
20
+
21
+ describe("client.data proxy — strict mode (collections dictionary provided)", () => {
22
+ const client = createRebaseClient({
23
+ baseUrl: "http://localhost:3000",
24
+ collections: { ...COLLECTIONS },
25
+ });
26
+
27
+ it("resolves a known key to a CollectionClient", () => {
28
+ const accessor = client.data.collection("products");
29
+ expect(accessor).toBeDefined();
30
+ expect(typeof accessor.find).toBe("function");
31
+ expect(typeof accessor.findById).toBe("function");
32
+ expect(typeof accessor.create).toBe("function");
33
+ });
34
+
35
+ it("resolves a known camelCase key (blogPosts) via dictionary", () => {
36
+ // Should not throw — blogPosts is in the dictionary
37
+ const products = (client.data as Record<string, unknown>)["products"];
38
+ expect(products).toBeDefined();
39
+ const blogPosts = (client.data as Record<string, unknown>)["blogPosts"];
40
+ expect(blogPosts).toBeDefined();
41
+ });
42
+
43
+ it("throws RebaseClientError on unknown key with suggestion", () => {
44
+ const access = () => (client.data as Record<string, unknown>)["prodcuts"];
45
+ expect(access).toThrow(RebaseClientError);
46
+
47
+ try {
48
+ access();
49
+ } catch (e) {
50
+ expect(e).toBeInstanceOf(RebaseClientError);
51
+ expect((e as RebaseClientError).message).toContain("prodcuts");
52
+ expect((e as RebaseClientError).message).toContain("products");
53
+ expect((e as RebaseClientError).message).toContain("Did you mean");
54
+ expect((e as RebaseClientError).message).toContain("data.collection");
55
+ }
56
+ });
57
+
58
+ it("throws RebaseClientError on unknown key without a close match", () => {
59
+ const access = () => (client.data as Record<string, unknown>)["zzz"];
60
+ expect(access).toThrow(RebaseClientError);
61
+
62
+ try {
63
+ access();
64
+ } catch (e) {
65
+ expect(e).toBeInstanceOf(RebaseClientError);
66
+ expect((e as RebaseClientError).message).toContain("zzz");
67
+ expect((e as RebaseClientError).message).not.toContain("Did you mean");
68
+ }
69
+ });
70
+
71
+ it("lists all known collections in the error message", () => {
72
+ const access = () => (client.data as Record<string, unknown>)["nonexistent"];
73
+ try {
74
+ access();
75
+ } catch (e) {
76
+ const msg = (e as RebaseClientError).message;
77
+ expect(msg).toContain("products");
78
+ expect(msg).toContain("orders");
79
+ expect(msg).toContain("blogPosts");
80
+ expect(msg).toContain("companyMembers");
81
+ }
82
+ });
83
+
84
+ it("returns undefined for symbol access", () => {
85
+ expect((client.data as Record<symbol, unknown>)[Symbol.toPrimitive]).toBeUndefined();
86
+ expect((client.data as Record<symbol, unknown>)[Symbol.iterator]).toBeUndefined();
87
+ });
88
+
89
+ it("returns undefined for 'then' (Promise resolution safe)", () => {
90
+ expect((client.data as Record<string, unknown>)["then"]).toBeUndefined();
91
+ });
92
+
93
+ it("returns undefined for 'toJSON'", () => {
94
+ expect((client.data as Record<string, unknown>)["toJSON"]).toBeUndefined();
95
+ });
96
+
97
+ it("returns undefined for '$$typeof' (React introspection safe)", () => {
98
+ expect((client.data as Record<string, unknown>)["$$typeof"]).toBeUndefined();
99
+ });
100
+
101
+ it("does not throw when awaited in a Promise.resolve context", async () => {
102
+ // `await obj` checks obj.then — which must return undefined, not throw.
103
+ const result = await Promise.resolve(client.data as Record<string, unknown>);
104
+ expect(result).toBeDefined();
105
+ });
106
+
107
+ it("collection('anything') never throws — dynamic escape hatch", () => {
108
+ expect(() => client.data.collection("nonexistent")).not.toThrow();
109
+ expect(() => client.data.collection("totally-made-up")).not.toThrow();
110
+ const col = client.data.collection("dynamic-slug");
111
+ expect(col).toBeDefined();
112
+ expect(typeof col.find).toBe("function");
113
+ });
114
+ });
115
+
116
+ describe("client.data proxy — untyped mode (no collections dictionary)", () => {
117
+ const client = createRebaseClient({
118
+ baseUrl: "http://localhost:3000",
119
+ });
120
+
121
+ it("falls back to snake_case conversion without a dictionary", () => {
122
+ // Should not throw for any property name
123
+ expect(() => (client.data as Record<string, unknown>)["blogPosts"]).not.toThrow();
124
+ expect(() => (client.data as Record<string, unknown>)["anyRandomProp"]).not.toThrow();
125
+ });
126
+
127
+ it("returns a CollectionClient for any camelCase property", () => {
128
+ const accessor = (client.data as Record<string, unknown>)["blogPosts"];
129
+ expect(accessor).toBeDefined();
130
+ expect(typeof (accessor as Record<string, unknown>).find).toBe("function");
131
+ });
132
+
133
+ it("returns undefined for symbols, then, toJSON, $$typeof", () => {
134
+ expect((client.data as Record<symbol, unknown>)[Symbol.toPrimitive]).toBeUndefined();
135
+ expect((client.data as Record<string, unknown>)["then"]).toBeUndefined();
136
+ expect((client.data as Record<string, unknown>)["toJSON"]).toBeUndefined();
137
+ expect((client.data as Record<string, unknown>)["$$typeof"]).toBeUndefined();
138
+ });
139
+
140
+ it("collection() still works without a dictionary", () => {
141
+ expect(() => client.data.collection("anything")).not.toThrow();
142
+ });
143
+
144
+ it("emits a one-shot console.warn on first untyped access", () => {
145
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
146
+ try {
147
+ // Create a fresh client so the warning flag is reset
148
+ const freshClient = createRebaseClient({
149
+ baseUrl: "http://localhost:3000",
150
+ });
151
+ const readFirst = () => (freshClient.data as Record<string, unknown>)["firstAccess"];
152
+ readFirst();
153
+ expect(warnSpy).toHaveBeenCalledTimes(1);
154
+ expect(warnSpy).toHaveBeenCalledWith(
155
+ expect.stringContaining("[Rebase] Untyped data access detected")
156
+ );
157
+
158
+ // Second access should NOT warn again
159
+ warnSpy.mockClear();
160
+ const readSecond = () => (freshClient.data as Record<string, unknown>)["secondAccess"];
161
+ readSecond();
162
+ expect(warnSpy).not.toHaveBeenCalled();
163
+ } finally {
164
+ warnSpy.mockRestore();
165
+ }
166
+ });
167
+ });
package/src/errors.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Client-side logic error (e.g. accessing an unknown collection when a typed
3
+ * dictionary is available). A subclass of {@link RebaseApiError}, so a single
4
+ * `catch (e) { if (e instanceof RebaseApiError) ... }` covers it too.
5
+ *
6
+ * The canonical definition now lives in `@rebasepro/types`; re-exported here to
7
+ * preserve the historical `import { RebaseClientError } from ".../errors"` path.
8
+ */
9
+ export { RebaseClientError } from "@rebasepro/types";