@rebasepro/client 0.8.0 → 0.9.1-canary.09aaf62
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/README.md +11 -11
- package/dist/admin.d.ts +1 -0
- package/dist/auth.d.ts +35 -38
- package/dist/backups.d.ts +13 -0
- package/dist/collection.d.ts +9 -13
- package/dist/errors.d.ts +9 -0
- package/dist/index.d.ts +38 -22
- package/dist/index.es.js +985 -271
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +89 -0
- package/dist/sdk_query_builder.d.ts +63 -0
- package/dist/transport.d.ts +36 -6
- package/dist/websocket.d.ts +92 -22
- package/package.json +8 -9
- package/src/admin.ts +1 -1
- package/src/api-keys.ts +1 -1
- package/src/auth.ts +188 -64
- package/src/backups.ts +40 -0
- package/src/collection.test.ts +92 -3
- package/src/collection.ts +88 -75
- package/src/data-proxy.test.ts +167 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +211 -28
- package/src/realtime-channel.test.ts +241 -0
- package/src/realtime-channel.ts +238 -0
- package/src/realtime-optout.test.ts +119 -0
- package/src/realtime-row-identity.test.ts +254 -0
- package/src/reviver.ts +2 -2
- package/src/sdk_query_builder.ts +141 -0
- package/src/storage.ts +64 -32
- package/src/transport.ts +59 -25
- package/src/websocket.ts +522 -193
- package/dist/collection.test.d.ts +0 -1
- package/dist/cron.test.d.ts +0 -1
- package/dist/index.umd.js +0 -2215
- package/dist/index.umd.js.map +0 -1
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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
|
-
*
|
|
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<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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<
|
|
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:
|
|
40
|
+
meta: FindResult<M>["meta"]
|
|
62
41
|
}>(basePath + qs, { method: "GET" });
|
|
63
42
|
return {
|
|
64
|
-
data: (raw.data || [])
|
|
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
|
|
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,23 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
88
67
|
method: "POST",
|
|
89
68
|
body: JSON.stringify(body)
|
|
90
69
|
});
|
|
91
|
-
return
|
|
70
|
+
return raw as M;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async createMany(data: Partial<M>[], options?: { upsert?: boolean }) {
|
|
74
|
+
if (!Array.isArray(data)) {
|
|
75
|
+
throw new TypeError("createMany expects an array of records.");
|
|
76
|
+
}
|
|
77
|
+
if (data.length === 0) return [];
|
|
78
|
+
|
|
79
|
+
const raw = await transport.request<{ data: Record<string, unknown>[] }>(`${basePath}/bulk`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
rows: data,
|
|
83
|
+
...(options?.upsert ? { upsert: true } : {})
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
return (raw.data || []) as M[];
|
|
92
87
|
},
|
|
93
88
|
|
|
94
89
|
async update(id: string | number, data: Partial<M>) {
|
|
@@ -96,11 +91,11 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
96
91
|
method: "PUT",
|
|
97
92
|
body: JSON.stringify(data)
|
|
98
93
|
});
|
|
99
|
-
return
|
|
94
|
+
return raw as M;
|
|
100
95
|
},
|
|
101
96
|
|
|
102
97
|
async delete(id: string | number) {
|
|
103
|
-
|
|
98
|
+
await transport.request<void>(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
104
99
|
method: "DELETE"
|
|
105
100
|
});
|
|
106
101
|
},
|
|
@@ -118,31 +113,31 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
118
113
|
|
|
119
114
|
// Fluent builder instantiation
|
|
120
115
|
where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown) {
|
|
121
|
-
const builder = new
|
|
116
|
+
const builder = new SDKQueryBuilder<M>(client);
|
|
122
117
|
if (typeof columnOrCondition === "object") {
|
|
123
118
|
return builder.where(columnOrCondition);
|
|
124
119
|
}
|
|
125
120
|
return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
|
|
126
121
|
},
|
|
127
122
|
orderBy(column: keyof M & string, direction?: "asc" | "desc") {
|
|
128
|
-
return new
|
|
123
|
+
return new SDKQueryBuilder<M>(client).orderBy(column, direction);
|
|
129
124
|
},
|
|
130
125
|
limit(count: number) {
|
|
131
|
-
return new
|
|
126
|
+
return new SDKQueryBuilder<M>(client).limit(count);
|
|
132
127
|
},
|
|
133
128
|
offset(count: number) {
|
|
134
|
-
return new
|
|
129
|
+
return new SDKQueryBuilder<M>(client).offset(count);
|
|
135
130
|
},
|
|
136
131
|
search(searchString: string) {
|
|
137
|
-
return new
|
|
132
|
+
return new SDKQueryBuilder<M>(client).search(searchString);
|
|
138
133
|
},
|
|
139
134
|
include(...relations: string[]) {
|
|
140
|
-
return new
|
|
135
|
+
return new SDKQueryBuilder<M>(client).include(...relations);
|
|
141
136
|
}
|
|
142
137
|
};
|
|
143
138
|
|
|
144
139
|
if (ws) {
|
|
145
|
-
client.listen = (params: FindParams | undefined, onUpdate: (response:
|
|
140
|
+
client.listen = (params: FindParams | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void) => {
|
|
146
141
|
let active = true;
|
|
147
142
|
let lastUpdateId = 0;
|
|
148
143
|
const unsub = ws.listenCollection(
|
|
@@ -151,45 +146,63 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
151
146
|
filter: params?.where,
|
|
152
147
|
limit: params?.limit,
|
|
153
148
|
startAfter: params?.offset ? String(params.offset) : undefined,
|
|
154
|
-
orderBy: params?.orderBy?.
|
|
155
|
-
order: params?.orderBy?.
|
|
149
|
+
orderBy: params?.orderBy?.[0],
|
|
150
|
+
order: params?.orderBy?.[1],
|
|
156
151
|
searchString: params?.searchString
|
|
157
152
|
},
|
|
158
|
-
(
|
|
153
|
+
(incomingRows: Record<string, unknown>[]) => {
|
|
159
154
|
const currentUpdateId = ++lastUpdateId;
|
|
160
155
|
const requestedLimit = params?.limit || 20;
|
|
161
156
|
const offset = params?.offset || 0;
|
|
162
157
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Asynchronously fetch the actual count from the server to get accurate total/hasMore
|
|
158
|
+
// WS client already delivers flat rows — just cast
|
|
159
|
+
const rows = incomingRows as M[];
|
|
160
|
+
|
|
161
|
+
// Heuristic metadata (used as fallback if count call fails)
|
|
162
|
+
const heuristicTotal = rows.length;
|
|
163
|
+
const heuristicHasMore = rows.length >= requestedLimit;
|
|
164
|
+
|
|
165
|
+
// Try to get authoritative count; fall back to heuristic
|
|
175
166
|
if (client.count) {
|
|
176
167
|
client.count(params)
|
|
177
168
|
.then((total) => {
|
|
178
169
|
if (active && currentUpdateId === lastUpdateId) {
|
|
179
170
|
onUpdate({
|
|
180
|
-
data:
|
|
171
|
+
data: rows,
|
|
181
172
|
meta: {
|
|
182
173
|
total,
|
|
183
174
|
limit: requestedLimit,
|
|
184
175
|
offset,
|
|
185
|
-
hasMore: offset +
|
|
176
|
+
hasMore: offset + rows.length < total
|
|
186
177
|
}
|
|
187
178
|
});
|
|
188
179
|
}
|
|
189
180
|
})
|
|
190
181
|
.catch(() => {
|
|
191
|
-
//
|
|
182
|
+
// Count failed — use heuristic meta
|
|
183
|
+
if (active && currentUpdateId === lastUpdateId) {
|
|
184
|
+
onUpdate({
|
|
185
|
+
data: rows,
|
|
186
|
+
meta: {
|
|
187
|
+
total: heuristicTotal,
|
|
188
|
+
limit: requestedLimit,
|
|
189
|
+
offset,
|
|
190
|
+
hasMore: heuristicHasMore
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
192
194
|
});
|
|
195
|
+
} else {
|
|
196
|
+
// No count method — fire immediately with heuristic meta
|
|
197
|
+
onUpdate({
|
|
198
|
+
data: rows,
|
|
199
|
+
meta: {
|
|
200
|
+
total: heuristicTotal,
|
|
201
|
+
limit: requestedLimit,
|
|
202
|
+
offset,
|
|
203
|
+
hasMore: heuristicHasMore
|
|
204
|
+
}
|
|
205
|
+
});
|
|
193
206
|
}
|
|
194
207
|
},
|
|
195
208
|
onError
|
|
@@ -201,15 +214,15 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
201
214
|
};
|
|
202
215
|
};
|
|
203
216
|
|
|
204
|
-
client.listenById = (id: string | number, onUpdate: (data:
|
|
205
|
-
return ws.
|
|
217
|
+
client.listenById = (id: string | number, onUpdate: (data: M | undefined) => void, onError?: (error: Error) => void) => {
|
|
218
|
+
return ws.listenOne(
|
|
206
219
|
{
|
|
207
220
|
path: slug,
|
|
208
|
-
|
|
221
|
+
id: String(id)
|
|
209
222
|
},
|
|
210
|
-
(
|
|
211
|
-
if (
|
|
212
|
-
onUpdate(
|
|
223
|
+
(row: Record<string, unknown> | null) => {
|
|
224
|
+
if (row) {
|
|
225
|
+
onUpdate(row as M);
|
|
213
226
|
} else {
|
|
214
227
|
onUpdate(undefined);
|
|
215
228
|
}
|
|
@@ -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";
|