@rebasepro/client 0.17.3-canary.gdd23447 → 0.18.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/README.md +4 -0
- package/dist/auth.d.ts +80 -0
- package/dist/functions.d.ts +6 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.es.js +471 -94
- package/dist/index.es.js.map +1 -1
- package/dist/offline-connectivity.d.ts +12 -1
- package/dist/offline.d.ts +23 -1
- package/dist/query-contract.types.d.ts +30 -0
- package/dist/realtime-channel.d.ts +29 -1
- package/dist/sdk_query_builder.d.ts +21 -2
- package/dist/transport.d.ts +24 -0
- package/package.json +28 -15
- package/src/admin.ts +0 -90
- package/src/anonymous-client-guard.test.ts +0 -190
- package/src/api-keys.ts +0 -87
- package/src/auth-listener-errors.test.ts +0 -57
- package/src/auth-refresh-overflow.test.ts +0 -89
- package/src/auth.ts +0 -982
- package/src/backups.ts +0 -40
- package/src/client-close.test.ts +0 -80
- package/src/collection-listen-meta.test.ts +0 -105
- package/src/collection-observe.test.ts +0 -138
- package/src/collection.test.ts +0 -293
- package/src/collection.ts +0 -525
- package/src/cron.test.ts +0 -164
- package/src/cron.ts +0 -62
- package/src/data-proxy.test.ts +0 -183
- package/src/errors.ts +0 -9
- package/src/functions.ts +0 -82
- package/src/index.ts +0 -639
- package/src/like-pattern-redos.test.ts +0 -61
- package/src/offline-codec.ts +0 -79
- package/src/offline-connectivity.test.ts +0 -191
- package/src/offline-connectivity.ts +0 -255
- package/src/offline-idb-store.test.ts +0 -340
- package/src/offline-integration.test.ts +0 -180
- package/src/offline-query.test.ts +0 -431
- package/src/offline-query.ts +0 -529
- package/src/offline-store.ts +0 -357
- package/src/offline-sync-engine.test.ts +0 -857
- package/src/offline.test.ts +0 -897
- package/src/offline.ts +0 -1928
- package/src/query-contract.types.ts +0 -206
- package/src/query_builder.ts +0 -1
- package/src/realtime-channel.test.ts +0 -542
- package/src/realtime-channel.ts +0 -539
- package/src/realtime-concurrent-subscribe.test.ts +0 -102
- package/src/realtime-error-surfacing.test.ts +0 -105
- package/src/realtime-optout.test.ts +0 -279
- package/src/realtime-row-identity.test.ts +0 -254
- package/src/realtime-subscription-key.test.ts +0 -92
- package/src/reviver.ts +0 -39
- package/src/sdk_query_builder.ts +0 -206
- package/src/storage-key-encoding.test.ts +0 -65
- package/src/storage-registry.ts +0 -102
- package/src/storage.ts +0 -253
- package/src/transport-baseurl.test.ts +0 -101
- package/src/transport.ts +0 -505
- package/src/vector-search-listen.test.ts +0 -42
- package/src/vector-search-query.test.ts +0 -55
- package/src/websocket-url.test.ts +0 -97
- package/src/websocket.ts +0 -1837
package/src/cron.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Transport } from "./transport";
|
|
2
|
-
import type { CronJobStatus, CronJobLogEntry } from "@rebasepro/types";
|
|
3
|
-
|
|
4
|
-
export interface CreateCronOptions {
|
|
5
|
-
cronPath?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function createCron(transport: Transport, options?: CreateCronOptions) {
|
|
9
|
-
const cronPath = options?.cronPath || "/cron";
|
|
10
|
-
|
|
11
|
-
async function listJobs(): Promise<{ jobs: CronJobStatus[] }> {
|
|
12
|
-
return transport.request<{ jobs: CronJobStatus[] }>(cronPath, { method: "GET" });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function getJob(jobId: string): Promise<{ job: CronJobStatus }> {
|
|
16
|
-
return transport.request<{ job: CronJobStatus }>(
|
|
17
|
-
cronPath + "/" + encodeURIComponent(jobId),
|
|
18
|
-
{ method: "GET" }
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function triggerJob(jobId: string): Promise<{ log: CronJobLogEntry; job: CronJobStatus }> {
|
|
23
|
-
return transport.request<{ log: CronJobLogEntry; job: CronJobStatus }>(
|
|
24
|
-
cronPath + "/" + encodeURIComponent(jobId) + "/trigger",
|
|
25
|
-
{ method: "POST" }
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function getJobLogs(
|
|
30
|
-
jobId: string,
|
|
31
|
-
options?: { limit?: number }
|
|
32
|
-
): Promise<{ logs: CronJobLogEntry[] }> {
|
|
33
|
-
const params = new URLSearchParams();
|
|
34
|
-
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
|
35
|
-
const qs = params.toString();
|
|
36
|
-
return transport.request<{ logs: CronJobLogEntry[] }>(
|
|
37
|
-
cronPath + "/" + encodeURIComponent(jobId) + "/logs" + (qs ? "?" + qs : ""),
|
|
38
|
-
{ method: "GET" }
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function toggleJob(
|
|
43
|
-
jobId: string,
|
|
44
|
-
enabled: boolean
|
|
45
|
-
): Promise<{ job: CronJobStatus }> {
|
|
46
|
-
return transport.request<{ job: CronJobStatus }>(
|
|
47
|
-
cronPath + "/" + encodeURIComponent(jobId),
|
|
48
|
-
{
|
|
49
|
-
method: "PUT",
|
|
50
|
-
body: JSON.stringify({ enabled })
|
|
51
|
-
}
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
listJobs,
|
|
57
|
-
getJob,
|
|
58
|
-
triggerJob,
|
|
59
|
-
getJobLogs,
|
|
60
|
-
toggleJob
|
|
61
|
-
};
|
|
62
|
-
}
|
package/src/data-proxy.test.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
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) to the dictionary's slug", async () => {
|
|
36
|
-
// The accessor is always "defined"; the whole job of the dictionary is
|
|
37
|
-
// that `blogPosts` addresses `blog-posts` on the wire — and NOT the
|
|
38
|
-
// `blog_posts` that the untyped snake_case fallback would produce.
|
|
39
|
-
// A client of its own: the shared one above captured `globalThis.fetch`
|
|
40
|
-
// when this file was collected, which is before `beforeAll` swaps it.
|
|
41
|
-
const fetchMock = jest.fn<typeof fetch>(async () => new Response("{}"));
|
|
42
|
-
const wired = createRebaseClient({
|
|
43
|
-
baseUrl: "http://localhost:3000",
|
|
44
|
-
fetch: fetchMock as typeof fetch,
|
|
45
|
-
collections: { ...COLLECTIONS }
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const products = (wired.data as Record<string, any>)["products"];
|
|
49
|
-
await products.find();
|
|
50
|
-
expect(String(fetchMock.mock.calls[0][0])).toContain("/data/products");
|
|
51
|
-
|
|
52
|
-
const blogPosts = (wired.data as Record<string, any>)["blogPosts"];
|
|
53
|
-
await blogPosts.find();
|
|
54
|
-
const url = String(fetchMock.mock.calls[1][0]);
|
|
55
|
-
expect(url).toContain("/data/blog-posts");
|
|
56
|
-
expect(url).not.toContain("blog_posts");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("throws RebaseClientError on unknown key with suggestion", () => {
|
|
60
|
-
const access = () => (client.data as Record<string, unknown>)["prodcuts"];
|
|
61
|
-
expect(access).toThrow(RebaseClientError);
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
access();
|
|
65
|
-
} catch (e) {
|
|
66
|
-
expect(e).toBeInstanceOf(RebaseClientError);
|
|
67
|
-
expect((e as RebaseClientError).message).toContain("prodcuts");
|
|
68
|
-
expect((e as RebaseClientError).message).toContain("products");
|
|
69
|
-
expect((e as RebaseClientError).message).toContain("Did you mean");
|
|
70
|
-
expect((e as RebaseClientError).message).toContain("data.collection");
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("throws RebaseClientError on unknown key without a close match", () => {
|
|
75
|
-
const access = () => (client.data as Record<string, unknown>)["zzz"];
|
|
76
|
-
expect(access).toThrow(RebaseClientError);
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
access();
|
|
80
|
-
} catch (e) {
|
|
81
|
-
expect(e).toBeInstanceOf(RebaseClientError);
|
|
82
|
-
expect((e as RebaseClientError).message).toContain("zzz");
|
|
83
|
-
expect((e as RebaseClientError).message).not.toContain("Did you mean");
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("lists all known collections in the error message", () => {
|
|
88
|
-
const access = () => (client.data as Record<string, unknown>)["nonexistent"];
|
|
89
|
-
try {
|
|
90
|
-
access();
|
|
91
|
-
} catch (e) {
|
|
92
|
-
const msg = (e as RebaseClientError).message;
|
|
93
|
-
expect(msg).toContain("products");
|
|
94
|
-
expect(msg).toContain("orders");
|
|
95
|
-
expect(msg).toContain("blogPosts");
|
|
96
|
-
expect(msg).toContain("companyMembers");
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("returns undefined for symbol access", () => {
|
|
101
|
-
expect((client.data as Record<symbol, unknown>)[Symbol.toPrimitive]).toBeUndefined();
|
|
102
|
-
expect((client.data as Record<symbol, unknown>)[Symbol.iterator]).toBeUndefined();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("returns undefined for 'then' (Promise resolution safe)", () => {
|
|
106
|
-
expect((client.data as Record<string, unknown>)["then"]).toBeUndefined();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("returns undefined for 'toJSON'", () => {
|
|
110
|
-
expect((client.data as Record<string, unknown>)["toJSON"]).toBeUndefined();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("returns undefined for '$$typeof' (React introspection safe)", () => {
|
|
114
|
-
expect((client.data as Record<string, unknown>)["$$typeof"]).toBeUndefined();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("does not throw when awaited in a Promise.resolve context", async () => {
|
|
118
|
-
// `await obj` checks obj.then — which must return undefined, not throw.
|
|
119
|
-
const result = await Promise.resolve(client.data as Record<string, unknown>);
|
|
120
|
-
expect(result).toBeDefined();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("collection('anything') never throws — dynamic escape hatch", () => {
|
|
124
|
-
expect(() => client.data.collection("nonexistent")).not.toThrow();
|
|
125
|
-
expect(() => client.data.collection("totally-made-up")).not.toThrow();
|
|
126
|
-
const col = client.data.collection("dynamic-slug");
|
|
127
|
-
expect(col).toBeDefined();
|
|
128
|
-
expect(typeof col.find).toBe("function");
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("client.data proxy — untyped mode (no collections dictionary)", () => {
|
|
133
|
-
const client = createRebaseClient({
|
|
134
|
-
baseUrl: "http://localhost:3000",
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("falls back to snake_case conversion without a dictionary", () => {
|
|
138
|
-
// Should not throw for any property name
|
|
139
|
-
expect(() => (client.data as Record<string, unknown>)["blogPosts"]).not.toThrow();
|
|
140
|
-
expect(() => (client.data as Record<string, unknown>)["anyRandomProp"]).not.toThrow();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("returns a CollectionClient for any camelCase property", () => {
|
|
144
|
-
const accessor = (client.data as Record<string, unknown>)["blogPosts"];
|
|
145
|
-
expect(accessor).toBeDefined();
|
|
146
|
-
expect(typeof (accessor as Record<string, unknown>).find).toBe("function");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("returns undefined for symbols, then, toJSON, $$typeof", () => {
|
|
150
|
-
expect((client.data as Record<symbol, unknown>)[Symbol.toPrimitive]).toBeUndefined();
|
|
151
|
-
expect((client.data as Record<string, unknown>)["then"]).toBeUndefined();
|
|
152
|
-
expect((client.data as Record<string, unknown>)["toJSON"]).toBeUndefined();
|
|
153
|
-
expect((client.data as Record<string, unknown>)["$$typeof"]).toBeUndefined();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("collection() still works without a dictionary", () => {
|
|
157
|
-
expect(() => client.data.collection("anything")).not.toThrow();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("emits a one-shot console.warn on first untyped access", () => {
|
|
161
|
-
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
162
|
-
try {
|
|
163
|
-
// Create a fresh client so the warning flag is reset
|
|
164
|
-
const freshClient = createRebaseClient({
|
|
165
|
-
baseUrl: "http://localhost:3000",
|
|
166
|
-
});
|
|
167
|
-
const readFirst = () => (freshClient.data as Record<string, unknown>)["firstAccess"];
|
|
168
|
-
readFirst();
|
|
169
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
170
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
171
|
-
expect.stringContaining("[Rebase] Untyped data access detected")
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Second access should NOT warn again
|
|
175
|
-
warnSpy.mockClear();
|
|
176
|
-
const readSecond = () => (freshClient.data as Record<string, unknown>)["secondAccess"];
|
|
177
|
-
readSecond();
|
|
178
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
179
|
-
} finally {
|
|
180
|
-
warnSpy.mockRestore();
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
});
|
package/src/errors.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
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";
|
package/src/functions.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { Transport } from "./transport";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Client interface for invoking custom backend functions.
|
|
5
|
-
*
|
|
6
|
-
* Custom functions are Hono route files auto-mounted by the Rebase backend
|
|
7
|
-
* at `/api/functions/{name}`. The `FunctionsClient` wraps the shared
|
|
8
|
-
* transport so callers never need to manually construct URLs or inject
|
|
9
|
-
* auth tokens.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* const result = await client.functions.invoke<{ job: Job }>('extract-job', {
|
|
14
|
-
* url: 'https://example.com/posting',
|
|
15
|
-
* html: htmlContent,
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export interface FunctionsClient {
|
|
20
|
-
/**
|
|
21
|
-
* Invoke a custom backend function by name.
|
|
22
|
-
*
|
|
23
|
-
* @typeParam T - Expected shape of the response payload.
|
|
24
|
-
* @param name - Function name (the filename without extension, e.g. `"extract-job"`).
|
|
25
|
-
* @param payload - Optional JSON-serialisable body sent as `POST`.
|
|
26
|
-
* @param options - Optional overrides (HTTP method, sub-path, extra headers).
|
|
27
|
-
* @returns The parsed JSON response from the function.
|
|
28
|
-
*/
|
|
29
|
-
invoke<T = unknown>(
|
|
30
|
-
name: string,
|
|
31
|
-
payload?: unknown,
|
|
32
|
-
options?: FunctionInvokeOptions,
|
|
33
|
-
): Promise<T>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type { FunctionInvokeOptions } from "@rebasepro/types";
|
|
37
|
-
import type { FunctionInvokeOptions } from "@rebasepro/types";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Create a `FunctionsClient` backed by the given transport.
|
|
41
|
-
*
|
|
42
|
-
* The transport already handles:
|
|
43
|
-
* - Base URL resolution
|
|
44
|
-
* - JWT injection via `Authorization: Bearer`
|
|
45
|
-
* - 401 retry / `onUnauthorized` flow
|
|
46
|
-
* - Consistent error throwing via `RebaseApiError`
|
|
47
|
-
*
|
|
48
|
-
* @internal
|
|
49
|
-
*/
|
|
50
|
-
export function createFunctionsClient(transport: Transport): FunctionsClient {
|
|
51
|
-
return {
|
|
52
|
-
async invoke<T = unknown>(
|
|
53
|
-
name: string,
|
|
54
|
-
payload?: unknown,
|
|
55
|
-
options?: FunctionInvokeOptions
|
|
56
|
-
): Promise<T> {
|
|
57
|
-
const method = options?.method ?? "POST";
|
|
58
|
-
// A `path` that starts the query or fragment is appended as-is. Only a
|
|
59
|
-
// real sub-path gets a separator: inserting one before `?days=30` asks
|
|
60
|
-
// for `/functions/dashboard-stats/?days=30`, and the trailing slash
|
|
61
|
-
// misses the route, so a function that exists answers 404 — and the
|
|
62
|
-
// caller sees it as the backend being down rather than as a bad URL.
|
|
63
|
-
const rawPath = options?.path;
|
|
64
|
-
const subPath = rawPath
|
|
65
|
-
? (/^[?#]/.test(rawPath) ? rawPath : `/${rawPath.replace(/^\//, "")}`)
|
|
66
|
-
: "";
|
|
67
|
-
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
68
|
-
|
|
69
|
-
const init: RequestInit = { method };
|
|
70
|
-
|
|
71
|
-
if (payload !== undefined && method !== "GET") {
|
|
72
|
-
init.body = JSON.stringify(payload);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (options?.headers) {
|
|
76
|
-
init.headers = options.headers;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return transport.request<T>(routePath, init);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|