@rebasepro/client 0.17.3 → 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.
Files changed (63) hide show
  1. package/README.md +4 -0
  2. package/dist/auth.d.ts +80 -0
  3. package/dist/functions.d.ts +6 -1
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.es.js +471 -94
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/offline-connectivity.d.ts +12 -1
  8. package/dist/offline.d.ts +23 -1
  9. package/dist/query-contract.types.d.ts +30 -0
  10. package/dist/realtime-channel.d.ts +29 -1
  11. package/dist/sdk_query_builder.d.ts +21 -2
  12. package/dist/transport.d.ts +24 -0
  13. package/package.json +28 -15
  14. package/src/admin.ts +0 -90
  15. package/src/anonymous-client-guard.test.ts +0 -190
  16. package/src/api-keys.ts +0 -87
  17. package/src/auth-listener-errors.test.ts +0 -57
  18. package/src/auth-refresh-overflow.test.ts +0 -89
  19. package/src/auth.ts +0 -982
  20. package/src/backups.ts +0 -40
  21. package/src/client-close.test.ts +0 -80
  22. package/src/collection-listen-meta.test.ts +0 -105
  23. package/src/collection-observe.test.ts +0 -138
  24. package/src/collection.test.ts +0 -293
  25. package/src/collection.ts +0 -525
  26. package/src/cron.test.ts +0 -164
  27. package/src/cron.ts +0 -62
  28. package/src/data-proxy.test.ts +0 -183
  29. package/src/errors.ts +0 -9
  30. package/src/functions.ts +0 -82
  31. package/src/index.ts +0 -639
  32. package/src/like-pattern-redos.test.ts +0 -61
  33. package/src/offline-codec.ts +0 -79
  34. package/src/offline-connectivity.test.ts +0 -191
  35. package/src/offline-connectivity.ts +0 -255
  36. package/src/offline-idb-store.test.ts +0 -340
  37. package/src/offline-integration.test.ts +0 -180
  38. package/src/offline-query.test.ts +0 -431
  39. package/src/offline-query.ts +0 -529
  40. package/src/offline-store.ts +0 -357
  41. package/src/offline-sync-engine.test.ts +0 -857
  42. package/src/offline.test.ts +0 -897
  43. package/src/offline.ts +0 -1928
  44. package/src/query-contract.types.ts +0 -206
  45. package/src/query_builder.ts +0 -1
  46. package/src/realtime-channel.test.ts +0 -542
  47. package/src/realtime-channel.ts +0 -539
  48. package/src/realtime-concurrent-subscribe.test.ts +0 -102
  49. package/src/realtime-error-surfacing.test.ts +0 -105
  50. package/src/realtime-optout.test.ts +0 -279
  51. package/src/realtime-row-identity.test.ts +0 -254
  52. package/src/realtime-subscription-key.test.ts +0 -92
  53. package/src/reviver.ts +0 -39
  54. package/src/sdk_query_builder.ts +0 -206
  55. package/src/storage-key-encoding.test.ts +0 -65
  56. package/src/storage-registry.ts +0 -102
  57. package/src/storage.ts +0 -253
  58. package/src/transport-baseurl.test.ts +0 -101
  59. package/src/transport.ts +0 -505
  60. package/src/vector-search-listen.test.ts +0 -42
  61. package/src/vector-search-query.test.ts +0 -55
  62. package/src/websocket-url.test.ts +0 -97
  63. 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
- }
@@ -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
- }