@rebasepro/client 0.7.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.
- package/LICENSE +21 -0
- package/README.md +10 -10
- package/dist/api-keys.d.ts +1 -0
- package/dist/auth.d.ts +35 -38
- package/dist/collection.d.ts +9 -13
- package/dist/data-proxy.test.d.ts +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/index.d.ts +41 -21
- package/dist/index.es.js +605 -368
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +616 -379
- package/dist/index.umd.js.map +1 -1
- package/dist/sdk_query_builder.d.ts +63 -0
- package/dist/storage-registry.d.ts +42 -0
- package/dist/storage.d.ts +9 -1
- package/dist/transport.d.ts +2 -6
- package/dist/websocket.d.ts +25 -21
- package/package.json +13 -12
- package/src/api-keys.ts +1 -0
- package/src/auth.ts +188 -64
- package/src/collection.test.ts +94 -5
- package/src/collection.ts +103 -184
- package/src/data-proxy.test.ts +167 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +218 -24
- package/src/reviver.ts +2 -2
- package/src/sdk_query_builder.ts +138 -0
- package/src/storage-registry.ts +102 -0
- package/src/storage.ts +92 -50
- package/src/transport.ts +31 -105
- package/src/websocket.ts +133 -135
package/src/index.ts
CHANGED
|
@@ -1,33 +1,99 @@
|
|
|
1
1
|
import { createTransport, RebaseClientConfig } from "./transport";
|
|
2
|
+
import { RebaseClientError } from "./errors";
|
|
2
3
|
import { createAuth, CreateAuthOptions } from "./auth";
|
|
3
4
|
import { createAdmin, CreateAdminOptions } from "./admin";
|
|
4
5
|
import { createCron, CreateCronOptions } from "./cron";
|
|
5
6
|
import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
|
|
6
|
-
import {
|
|
7
|
+
import { CollectionClient, createCollectionClient } from "./collection";
|
|
7
8
|
import { createFunctionsClient } from "./functions";
|
|
8
9
|
import { createStorage } from "./storage";
|
|
10
|
+
import { ClientStorageSourceRegistry } from "./storage-registry";
|
|
9
11
|
import { RebaseWebSocketClient } from "./websocket";
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_STORAGE_SOURCE_KEY,
|
|
14
|
+
InsertOf,
|
|
15
|
+
RebaseClient,
|
|
16
|
+
RebaseSdkData,
|
|
17
|
+
RowOf,
|
|
18
|
+
StorageSource,
|
|
19
|
+
StorageSourceDefinition,
|
|
20
|
+
StorageSourceRegistry,
|
|
21
|
+
UpdateOf
|
|
22
|
+
} from "@rebasepro/types";
|
|
11
23
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// ─── Public API surface ──────────────────────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// This barrel is the public API of `@rebasepro/client`. It is an explicit,
|
|
28
|
+
// curated list — NOT `export *` — so that adding an export to a module below
|
|
29
|
+
// does not silently republish it to app developers. Internal factories
|
|
30
|
+
// (`createTransport`, `createAuth`, `createCollectionClient`, …), the raw
|
|
31
|
+
// `Transport`, the storage-source registry impl, the JSON reviver, and the
|
|
32
|
+
// concrete `SDKQueryBuilder` class are intentionally NOT re-exported: they are
|
|
33
|
+
// implementation details of `createRebaseClient()` and have no external
|
|
34
|
+
// consumers. App developers reach them through the client instance, never by
|
|
35
|
+
// importing the factory. To add something to the public surface, add it here
|
|
36
|
+
// deliberately.
|
|
37
|
+
|
|
38
|
+
// Errors — the single error type thrown by SDK HTTP calls, plus the
|
|
39
|
+
// data-proxy's unknown-collection error.
|
|
40
|
+
export { RebaseApiError } from "./transport";
|
|
41
|
+
export { RebaseClientError } from "./errors";
|
|
42
|
+
|
|
43
|
+
// Query + collection types (annotate SDK results; construct via the fluent API).
|
|
44
|
+
export type { RebaseClientConfig, FindParams, FindResponse } from "./transport";
|
|
45
|
+
export type { CollectionClient } from "./collection";
|
|
46
|
+
export type { FindResult, SDKCollectionClient, SDKQueryBuilderInterface, PaginationMeta } from "@rebasepro/types";
|
|
47
|
+
|
|
48
|
+
// Logical-condition helpers for `.where(or(...), and(...))`.
|
|
49
|
+
export { QueryBuilder, or, and, cond } from "@rebasepro/common";
|
|
50
|
+
|
|
51
|
+
// Auth: session/token types, config, and the pluggable storage strategies.
|
|
52
|
+
export { createCookieStorage, createMemoryStorage } from "./auth";
|
|
53
|
+
export type { AuthConfig, AuthStorage, CookieStorageOptions, CreateAuthOptions } from "./auth";
|
|
54
|
+
export type { RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
55
|
+
/** @deprecated Import `User` / `AuthTokens` from `@rebasepro/types` instead. */
|
|
56
|
+
export type { RebaseUser, RebaseTokens } from "./auth";
|
|
57
|
+
|
|
58
|
+
// Control-plane client option/DTO types (the client instance exposes the impls).
|
|
59
|
+
export type { CreateAdminOptions } from "./admin";
|
|
60
|
+
export type { AdminUser } from "./admin";
|
|
61
|
+
export type { CreateCronOptions } from "./cron";
|
|
62
|
+
export type {
|
|
63
|
+
ApiKeyMasked,
|
|
64
|
+
ApiKeyPermission,
|
|
65
|
+
ApiKeyWithSecret,
|
|
66
|
+
CreateApiKeyRequest,
|
|
67
|
+
CreateApiKeysOptions,
|
|
68
|
+
UpdateApiKeyRequest
|
|
69
|
+
} from "./api-keys";
|
|
70
|
+
export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
|
|
71
|
+
|
|
72
|
+
// Realtime: the WebSocket client class is internal to `createRebaseClient()`,
|
|
73
|
+
// but re-exported (see @internal on the class) because the `client-postgresql`
|
|
74
|
+
// driver constructs it directly. Not a stable app-facing API.
|
|
75
|
+
export { RebaseWebSocketClient } from "./websocket";
|
|
25
76
|
|
|
26
77
|
export interface CreateRebaseClientOptions extends RebaseClientConfig {
|
|
27
78
|
auth?: CreateAuthOptions;
|
|
28
79
|
admin?: CreateAdminOptions;
|
|
29
80
|
cron?: CreateCronOptions;
|
|
30
81
|
apiKeys?: CreateApiKeysOptions;
|
|
82
|
+
/**
|
|
83
|
+
* Declared storage sources for multi-backend support. Server-transport
|
|
84
|
+
* entries are auto-wired into `client.storageRegistry`; `direct` sources
|
|
85
|
+
* are registered app-side (e.g. via a Firebase Storage hook). The default
|
|
86
|
+
* source (`storage`) is always registered under
|
|
87
|
+
* {@link DEFAULT_STORAGE_SOURCE_KEY}.
|
|
88
|
+
*/
|
|
89
|
+
storageSources?: StorageSourceDefinition[];
|
|
90
|
+
/**
|
|
91
|
+
* Maps camelCase property names / safe identifiers to the actual
|
|
92
|
+
* collection slugs on the server (e.g. `{ companyMembers: "company-members" }`).
|
|
93
|
+
* If provided, the data layer proxy will resolve property accessors to their
|
|
94
|
+
* correct slugs via this map before falling back to automatic snake_casing.
|
|
95
|
+
*/
|
|
96
|
+
collections?: Record<string, string>;
|
|
31
97
|
}
|
|
32
98
|
|
|
33
99
|
// ─── Typed Data Proxy ────────────────────────────────────────────────────────
|
|
@@ -38,17 +104,21 @@ type KebabToCamelCase<S extends string> =
|
|
|
38
104
|
? `${T}${Capitalize<KebabToCamelCase<U>>}`
|
|
39
105
|
: S;
|
|
40
106
|
|
|
107
|
+
// Resolve a generated `Database` entry from a (kebab-case) slug literal,
|
|
108
|
+
// or `unknown` when the slug isn't in the schema — the extractors below
|
|
109
|
+
// then fall back to the open row / partial shapes.
|
|
110
|
+
type DBEntry<DB, S extends string> =
|
|
111
|
+
KebabToCamelCase<S> extends keyof DB ? DB[KebabToCamelCase<S>] : unknown;
|
|
112
|
+
|
|
41
113
|
type TypedDataLayer<DB> = {
|
|
42
114
|
collection<S extends string>(slug: S): CollectionClient<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
115
|
+
RowOf<DBEntry<DB, S>>,
|
|
116
|
+
InsertOf<DBEntry<DB, S>>,
|
|
117
|
+
UpdateOf<DBEntry<DB, S>>
|
|
46
118
|
>;
|
|
47
119
|
} & {
|
|
48
|
-
[K in keyof DB]: CollectionClient<
|
|
49
|
-
|
|
50
|
-
>;
|
|
51
|
-
} & RebaseData;
|
|
120
|
+
[K in keyof DB]: CollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>>;
|
|
121
|
+
} & RebaseSdkData;
|
|
52
122
|
|
|
53
123
|
/**
|
|
54
124
|
* The return type of `createRebaseClient<DB>()`.
|
|
@@ -57,7 +127,7 @@ type TypedDataLayer<DB> = {
|
|
|
57
127
|
* capabilities populated and the `data` layer narrowed to provide
|
|
58
128
|
* typed collection accessors when a `DB` schema generic is supplied.
|
|
59
129
|
*/
|
|
60
|
-
export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<RebaseClient
|
|
130
|
+
export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<RebaseClient<DB>, "data" | "email"> & {
|
|
61
131
|
setToken: (token: string | null) => void;
|
|
62
132
|
setAuthTokenGetter: (getter: () => Promise<string | null>) => void;
|
|
63
133
|
setOnUnauthorized: (handler: () => Promise<boolean>) => void;
|
|
@@ -69,6 +139,9 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
69
139
|
functions: ReturnType<typeof createFunctionsClient>;
|
|
70
140
|
ws?: RebaseWebSocketClient;
|
|
71
141
|
storage: StorageSource;
|
|
142
|
+
storageRegistry: StorageSourceRegistry;
|
|
143
|
+
createStorageSource: (storageId: string) => StorageSource;
|
|
144
|
+
fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
|
|
72
145
|
call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
|
|
73
146
|
data: TypedDataLayer<DB>;
|
|
74
147
|
};
|
|
@@ -118,6 +191,48 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
118
191
|
const storage = createStorage(transport);
|
|
119
192
|
const functions = createFunctionsClient(transport);
|
|
120
193
|
|
|
194
|
+
// Build a server-backed StorageSource for a given storage-source key.
|
|
195
|
+
const createStorageSource = (storageId: string): StorageSource =>
|
|
196
|
+
storageId === DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
|
|
197
|
+
|
|
198
|
+
// Storage registry: always holds the default source, plus any declared
|
|
199
|
+
// server-transport sources. `direct` sources are registered app-side.
|
|
200
|
+
const storageRegistry = new ClientStorageSourceRegistry();
|
|
201
|
+
storageRegistry.register(DEFAULT_STORAGE_SOURCE_KEY, storage);
|
|
202
|
+
for (const def of options.storageSources ?? []) {
|
|
203
|
+
if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY) {
|
|
204
|
+
storageRegistry.register(def.key, createStorageSource(def.key));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Discover storage sources from the backend, making the server the single
|
|
209
|
+
// source of truth. Server-transport sources are auto-wired into the
|
|
210
|
+
// registry; `direct` sources are returned for the app to register. The
|
|
211
|
+
// promise is cached on success and reset on failure so it can be retried
|
|
212
|
+
// (e.g. once the user authenticates).
|
|
213
|
+
let storageSourcesPromise: Promise<StorageSourceDefinition[]> | undefined;
|
|
214
|
+
const fetchStorageSources = (): Promise<StorageSourceDefinition[]> => {
|
|
215
|
+
if (storageSourcesPromise) return storageSourcesPromise;
|
|
216
|
+
storageSourcesPromise = transport
|
|
217
|
+
.request<{ data: StorageSourceDefinition[] }>("/storage/sources")
|
|
218
|
+
.then((res) => {
|
|
219
|
+
const defs = res.data ?? [];
|
|
220
|
+
for (const def of defs) {
|
|
221
|
+
if (def.transport === "server"
|
|
222
|
+
&& def.key !== DEFAULT_STORAGE_SOURCE_KEY
|
|
223
|
+
&& !storageRegistry.has(def.key)) {
|
|
224
|
+
storageRegistry.register(def.key, createStorageSource(def.key));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return defs;
|
|
228
|
+
})
|
|
229
|
+
.catch((e) => {
|
|
230
|
+
storageSourcesPromise = undefined; // allow retry
|
|
231
|
+
throw e;
|
|
232
|
+
});
|
|
233
|
+
return storageSourcesPromise;
|
|
234
|
+
};
|
|
235
|
+
|
|
121
236
|
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
122
237
|
|
|
123
238
|
let ws: RebaseWebSocketClient | undefined;
|
|
@@ -172,7 +287,62 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
172
287
|
});
|
|
173
288
|
}
|
|
174
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Suggest the closest known collection key for a mistyped accessor.
|
|
292
|
+
* Uses edit-distance-1 and prefix matching — no external dependency.
|
|
293
|
+
*/
|
|
294
|
+
function suggestCollection(prop: string, knownKeys: string[]): string | undefined {
|
|
295
|
+
// Prefix match (e.g. "prod" → "products")
|
|
296
|
+
const prefixMatch = knownKeys.find(k => k.startsWith(prop) || prop.startsWith(k));
|
|
297
|
+
if (prefixMatch) return prefixMatch;
|
|
298
|
+
|
|
299
|
+
// Edit-distance-1: deletions, insertions, substitutions, transpositions
|
|
300
|
+
for (const key of knownKeys) {
|
|
301
|
+
if (Math.abs(key.length - prop.length) > 1) continue;
|
|
302
|
+
let diffs = 0;
|
|
303
|
+
const longer = key.length >= prop.length ? key : prop;
|
|
304
|
+
const shorter = key.length >= prop.length ? prop : key;
|
|
305
|
+
if (longer.length === shorter.length) {
|
|
306
|
+
// Same length: allow 1 substitution or 1 transposition
|
|
307
|
+
for (let i = 0; i < longer.length; i++) {
|
|
308
|
+
if (longer[i] !== shorter[i]) {
|
|
309
|
+
// Check for transposition
|
|
310
|
+
if (
|
|
311
|
+
i + 1 < longer.length &&
|
|
312
|
+
longer[i] === shorter[i + 1] &&
|
|
313
|
+
longer[i + 1] === shorter[i]
|
|
314
|
+
) {
|
|
315
|
+
diffs++;
|
|
316
|
+
i++; // skip next char (already accounted for)
|
|
317
|
+
if (diffs > 1) break;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
diffs++;
|
|
321
|
+
}
|
|
322
|
+
if (diffs > 1) break;
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
// Length differs by 1: allow 1 insertion/deletion
|
|
326
|
+
let li = 0;
|
|
327
|
+
let si = 0;
|
|
328
|
+
while (li < longer.length) {
|
|
329
|
+
if (si < shorter.length && longer[li] === shorter[si]) {
|
|
330
|
+
si++;
|
|
331
|
+
} else {
|
|
332
|
+
diffs++;
|
|
333
|
+
}
|
|
334
|
+
li++;
|
|
335
|
+
if (diffs > 1) break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (diffs <= 1) return key;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
175
344
|
const collectionClients = new Map<string, CollectionClient<Record<string, unknown>>>();
|
|
345
|
+
let untypedWarned = false;
|
|
176
346
|
|
|
177
347
|
function collection(slug: string): CollectionClient<Record<string, unknown>> {
|
|
178
348
|
if (!collectionClients.has(slug)) {
|
|
@@ -190,8 +360,30 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
190
360
|
}
|
|
191
361
|
if (typeof prop === "symbol") return undefined;
|
|
192
362
|
if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
|
|
193
|
-
|
|
363
|
+
if (options.collections) {
|
|
364
|
+
if (prop in options.collections) {
|
|
365
|
+
return collection(options.collections[prop]);
|
|
366
|
+
}
|
|
367
|
+
// Strict mode: the developer supplied a typed dictionary,
|
|
368
|
+
// so we know the full set of valid accessors.
|
|
369
|
+
const knownKeys = Object.keys(options.collections);
|
|
370
|
+
const suggestion = suggestCollection(prop, knownKeys);
|
|
371
|
+
const knownList = knownKeys.join(", ");
|
|
372
|
+
let msg = `Unknown collection accessor "${prop}". Known collections: ${knownList}.`;
|
|
373
|
+
if (suggestion) msg += ` Did you mean "${suggestion}"?`;
|
|
374
|
+
msg += ` Use data.collection("<slug>") for dynamic slugs.`;
|
|
375
|
+
throw new RebaseClientError(msg);
|
|
376
|
+
}
|
|
377
|
+
// Untyped fallback: convert camelCase property names to snake_case slugs.
|
|
194
378
|
// e.g. `companyMembers` → `company_members`
|
|
379
|
+
if (!untypedWarned) {
|
|
380
|
+
untypedWarned = true;
|
|
381
|
+
console.warn(
|
|
382
|
+
`[Rebase] Untyped data access detected (client.data.${prop}). ` +
|
|
383
|
+
`Collection names are resolved via snake_case conversion, which may cause silent 404s at request time. ` +
|
|
384
|
+
`Pass a \`collections\` dictionary to createRebaseClient() or use the generated SDK for type-safe access.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
195
387
|
const slug = toSnakeCase(prop);
|
|
196
388
|
return collection(slug);
|
|
197
389
|
}
|
|
@@ -206,6 +398,9 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
206
398
|
apiKeys,
|
|
207
399
|
functions,
|
|
208
400
|
storage,
|
|
401
|
+
storageRegistry,
|
|
402
|
+
createStorageSource,
|
|
403
|
+
fetchStorageSources,
|
|
209
404
|
ws,
|
|
210
405
|
setToken: transport.setToken,
|
|
211
406
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
@@ -222,7 +417,6 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
222
417
|
return res.data ?? (res as T);
|
|
223
418
|
},
|
|
224
419
|
data: dataProxy,
|
|
225
|
-
email: undefined
|
|
226
420
|
} as unknown as CreateRebaseClientResult<DB>;
|
|
227
421
|
|
|
228
422
|
return target;
|
package/src/reviver.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityReference, EntityRelation, GeoPoint, Vector
|
|
1
|
+
import { EntityReference, EntityRelation, GeoPoint, Vector } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
export function rebaseReviver(_key: string, value: unknown): unknown {
|
|
4
4
|
if (value && typeof value === "object" && "__type" in value) {
|
|
@@ -25,7 +25,7 @@ export function rebaseReviver(_key: string, value: unknown): unknown {
|
|
|
25
25
|
return new EntityRelation(
|
|
26
26
|
record.id as string | number,
|
|
27
27
|
record.path as string,
|
|
28
|
-
record.data as
|
|
28
|
+
record.data as Record<string, unknown> | undefined
|
|
29
29
|
);
|
|
30
30
|
case "GeoPoint":
|
|
31
31
|
return new GeoPoint(record.latitude as number, record.longitude as number);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FindParams,
|
|
3
|
+
FindResult,
|
|
4
|
+
LogicalCondition,
|
|
5
|
+
SDKCollectionClient,
|
|
6
|
+
SDKQueryBuilderInterface,
|
|
7
|
+
WhereFilterOp,
|
|
8
|
+
WhereValue
|
|
9
|
+
} from "@rebasepro/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SDK Query Builder — returns flat rows (`FindResult<M>`) instead of
|
|
13
|
+
* Entity-wrapped results (`FindResponse<M>`).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const { data } = await rebase.data.posts
|
|
17
|
+
* .where("status", "==", "published")
|
|
18
|
+
* .orderBy("created_at", "desc")
|
|
19
|
+
* .limit(10)
|
|
20
|
+
* .find();
|
|
21
|
+
*
|
|
22
|
+
* console.log(data[0].title); // flat access
|
|
23
|
+
*/
|
|
24
|
+
export class SDKQueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements SDKQueryBuilderInterface<M> {
|
|
25
|
+
private params: FindParams = { where: {} };
|
|
26
|
+
|
|
27
|
+
constructor(private collection: SDKCollectionClient<M>) {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a filter condition to your query.
|
|
31
|
+
* @example
|
|
32
|
+
* client.data.users.where('age', '>=', 18).find()
|
|
33
|
+
*/
|
|
34
|
+
where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
|
|
35
|
+
where(logicalCondition: LogicalCondition): this;
|
|
36
|
+
where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown): this {
|
|
37
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
38
|
+
this.params.logical = columnOrCondition as LogicalCondition;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.params.where) {
|
|
43
|
+
this.params.where = {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const column = columnOrCondition as string;
|
|
47
|
+
const condition: [WhereFilterOp, unknown] = [operator!, value];
|
|
48
|
+
const existing = this.params.where[column];
|
|
49
|
+
|
|
50
|
+
if (existing === undefined) {
|
|
51
|
+
this.params.where[column] = condition;
|
|
52
|
+
} else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {
|
|
53
|
+
(this.params.where[column] as [WhereFilterOp, unknown][]).push(condition);
|
|
54
|
+
} else {
|
|
55
|
+
let firstCondition: [WhereFilterOp, unknown];
|
|
56
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") {
|
|
57
|
+
firstCondition = existing as [WhereFilterOp, unknown];
|
|
58
|
+
} else {
|
|
59
|
+
firstCondition = ["==", existing];
|
|
60
|
+
}
|
|
61
|
+
this.params.where[column] = [firstCondition, condition];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Order the results by a specific column.
|
|
69
|
+
*/
|
|
70
|
+
orderBy(column: keyof M & string, direction: "asc" | "desc" = "asc"): this {
|
|
71
|
+
this.params.orderBy = [column, direction];
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Limit the number of results returned.
|
|
77
|
+
*/
|
|
78
|
+
limit(count: number): this {
|
|
79
|
+
this.params.limit = count;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Skip the first N results.
|
|
85
|
+
*/
|
|
86
|
+
offset(count: number): this {
|
|
87
|
+
this.params.offset = count;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Set a free-text search string if supported by the backend.
|
|
93
|
+
*/
|
|
94
|
+
search(searchString: string): this {
|
|
95
|
+
this.params.searchString = searchString;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Include related entities in the response.
|
|
101
|
+
* Relations will be populated with full data instead of just IDs.
|
|
102
|
+
*
|
|
103
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
104
|
+
* @example
|
|
105
|
+
* client.data.posts.include("tags", "author").find()
|
|
106
|
+
*/
|
|
107
|
+
include(...relations: string[]): this {
|
|
108
|
+
this.params.include = relations;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Execute the find query and return the results as flat rows.
|
|
114
|
+
*/
|
|
115
|
+
async find(): Promise<FindResult<M>> {
|
|
116
|
+
return this.collection.find(this.params);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Count the records matching this query.
|
|
121
|
+
*/
|
|
122
|
+
async count(): Promise<number> {
|
|
123
|
+
if (!this.collection.count) {
|
|
124
|
+
throw new Error("count() is not supported by this collection client.");
|
|
125
|
+
}
|
|
126
|
+
return this.collection.count(this.params);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Listen to realtime updates matching this query.
|
|
131
|
+
*/
|
|
132
|
+
listen(onUpdate: (data: FindResult<M>) => void, onError?: (error: Error) => void): () => void {
|
|
133
|
+
if (!this.collection.listen) {
|
|
134
|
+
throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
135
|
+
}
|
|
136
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side storage source registry.
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple `StorageSource` instances keyed by
|
|
5
|
+
* `StorageSourceDefinition.key`. Collection properties reference
|
|
6
|
+
* a source by key via `StorageConfig.storageSource`.
|
|
7
|
+
*
|
|
8
|
+
* Typical bootstrap flow:
|
|
9
|
+
* 1. Fetch definitions from `GET /api/storage/sources`
|
|
10
|
+
* 2. Build server-backed sources automatically via `createStorage(transport, key)`
|
|
11
|
+
* 3. Register "direct" sources manually (e.g. Firebase Storage hook)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StorageSource, StorageSourceRegistry, StorageSourceDefinition } from "@rebasepro/types";
|
|
15
|
+
import { DEFAULT_STORAGE_SOURCE_KEY } from "@rebasepro/types";
|
|
16
|
+
import { createStorage } from "./storage";
|
|
17
|
+
import type { Transport } from "./transport";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default implementation of the client-side `StorageSourceRegistry`.
|
|
21
|
+
*/
|
|
22
|
+
export class ClientStorageSourceRegistry implements StorageSourceRegistry {
|
|
23
|
+
private sources = new Map<string, StorageSource>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register a storage source.
|
|
27
|
+
* @param key - Unique key matching a `StorageSourceDefinition.key`
|
|
28
|
+
* @param source - The `StorageSource` instance
|
|
29
|
+
*/
|
|
30
|
+
register(key: string, source: StorageSource): void {
|
|
31
|
+
this.sources.set(key, source);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getDefault(): StorageSource {
|
|
35
|
+
const source = this.sources.get(DEFAULT_STORAGE_SOURCE_KEY);
|
|
36
|
+
if (!source) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[StorageSourceRegistry] No default storage source registered. ` +
|
|
39
|
+
`Register one with key "${DEFAULT_STORAGE_SOURCE_KEY}".`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return source;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(key: string | undefined | null): StorageSource | undefined {
|
|
46
|
+
if (key === undefined || key === null) {
|
|
47
|
+
return this.sources.get(DEFAULT_STORAGE_SOURCE_KEY);
|
|
48
|
+
}
|
|
49
|
+
return this.sources.get(key);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getOrDefault(key: string | undefined | null): StorageSource {
|
|
53
|
+
if (key === undefined || key === null) {
|
|
54
|
+
return this.getDefault();
|
|
55
|
+
}
|
|
56
|
+
const source = this.sources.get(key);
|
|
57
|
+
if (source) return source;
|
|
58
|
+
|
|
59
|
+
// Fallback to default
|
|
60
|
+
console.warn(
|
|
61
|
+
`[StorageSourceRegistry] Storage source "${key}" not found, ` +
|
|
62
|
+
`falling back to "${DEFAULT_STORAGE_SOURCE_KEY}".`
|
|
63
|
+
);
|
|
64
|
+
return this.getDefault();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
has(key: string): boolean {
|
|
68
|
+
return this.sources.has(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
list(): string[] {
|
|
72
|
+
return Array.from(this.sources.keys());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a registry from `StorageSourceDefinition[]` and an HTTP transport.
|
|
77
|
+
*
|
|
78
|
+
* - Sources with `transport: "server"` are auto-wired via `createStorage(transport, key)`.
|
|
79
|
+
* - Sources with `transport: "direct"` are **not** auto-wired — they must
|
|
80
|
+
* be registered manually after this call (e.g. via a Firebase hook).
|
|
81
|
+
*
|
|
82
|
+
* @param definitions - Array of storage source definitions
|
|
83
|
+
* @param transport - HTTP transport for server-backed sources
|
|
84
|
+
*/
|
|
85
|
+
static fromDefinitions(
|
|
86
|
+
definitions: StorageSourceDefinition[],
|
|
87
|
+
transport: Transport
|
|
88
|
+
): ClientStorageSourceRegistry {
|
|
89
|
+
const registry = new ClientStorageSourceRegistry();
|
|
90
|
+
|
|
91
|
+
for (const def of definitions) {
|
|
92
|
+
if (def.transport === "server") {
|
|
93
|
+
// Auto-create a server-backed StorageSource for this key
|
|
94
|
+
const source = createStorage(transport, def.key === DEFAULT_STORAGE_SOURCE_KEY ? undefined : def.key);
|
|
95
|
+
registry.register(def.key, source);
|
|
96
|
+
}
|
|
97
|
+
// "direct" sources must be registered manually
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return registry;
|
|
101
|
+
}
|
|
102
|
+
}
|