@rebasepro/client 0.7.0 → 0.8.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/dist/api-keys.d.ts +1 -0
- package/dist/collection.d.ts +3 -3
- package/dist/index.d.ts +20 -1
- package/dist/index.es.js +135 -168
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +138 -170
- package/dist/index.umd.js.map +1 -1
- package/dist/storage-registry.d.ts +42 -0
- package/dist/storage.d.ts +9 -1
- package/package.json +13 -12
- package/src/api-keys.ts +1 -0
- package/src/collection.test.ts +2 -2
- package/src/collection.ts +41 -119
- package/src/index.ts +77 -2
- package/src/storage-registry.ts +102 -0
- package/src/storage.ts +30 -20
- package/src/transport.ts +8 -82
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
import type { StorageSource, StorageSourceRegistry, StorageSourceDefinition } from "@rebasepro/types";
|
|
14
|
+
import type { Transport } from "./transport";
|
|
15
|
+
/**
|
|
16
|
+
* Default implementation of the client-side `StorageSourceRegistry`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ClientStorageSourceRegistry implements StorageSourceRegistry {
|
|
19
|
+
private sources;
|
|
20
|
+
/**
|
|
21
|
+
* Register a storage source.
|
|
22
|
+
* @param key - Unique key matching a `StorageSourceDefinition.key`
|
|
23
|
+
* @param source - The `StorageSource` instance
|
|
24
|
+
*/
|
|
25
|
+
register(key: string, source: StorageSource): void;
|
|
26
|
+
getDefault(): StorageSource;
|
|
27
|
+
get(key: string | undefined | null): StorageSource | undefined;
|
|
28
|
+
getOrDefault(key: string | undefined | null): StorageSource;
|
|
29
|
+
has(key: string): boolean;
|
|
30
|
+
list(): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Build a registry from `StorageSourceDefinition[]` and an HTTP transport.
|
|
33
|
+
*
|
|
34
|
+
* - Sources with `transport: "server"` are auto-wired via `createStorage(transport, key)`.
|
|
35
|
+
* - Sources with `transport: "direct"` are **not** auto-wired — they must
|
|
36
|
+
* be registered manually after this call (e.g. via a Firebase hook).
|
|
37
|
+
*
|
|
38
|
+
* @param definitions - Array of storage source definitions
|
|
39
|
+
* @param transport - HTTP transport for server-backed sources
|
|
40
|
+
*/
|
|
41
|
+
static fromDefinitions(definitions: StorageSourceDefinition[], transport: Transport): ClientStorageSourceRegistry;
|
|
42
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { StorageSource } from "@rebasepro/types";
|
|
2
2
|
import { Transport } from "./transport";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Create a StorageSource that talks to the Rebase backend REST API.
|
|
5
|
+
*
|
|
6
|
+
* @param transport - HTTP transport instance
|
|
7
|
+
* @param storageId - Optional storage-source key for multi-backend routing.
|
|
8
|
+
* When set, it is forwarded to the server so the correct
|
|
9
|
+
* `StorageController` is resolved from the registry.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createStorage(transport: Transport, storageId?: string): StorageSource;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "HTTP SDK client for the Rebase custom backend",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -20,13 +20,6 @@
|
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=14"
|
|
22
22
|
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"watch": "vite build --watch",
|
|
25
|
-
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
|
|
26
|
-
"test:lint": "eslint \"src/**\" --quiet",
|
|
27
|
-
"test": "jest --passWithNoTests --forceExit",
|
|
28
|
-
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
|
|
29
|
-
},
|
|
30
23
|
"exports": {
|
|
31
24
|
".": {
|
|
32
25
|
"types": "./dist/index.d.ts",
|
|
@@ -37,9 +30,9 @@
|
|
|
37
30
|
"./package.json": "./package.json"
|
|
38
31
|
},
|
|
39
32
|
"dependencies": {
|
|
40
|
-
"@rebasepro/common": "
|
|
41
|
-
"@rebasepro/types": "
|
|
42
|
-
"@rebasepro/utils": "
|
|
33
|
+
"@rebasepro/common": "0.8.0",
|
|
34
|
+
"@rebasepro/types": "0.8.0",
|
|
35
|
+
"@rebasepro/utils": "0.8.0"
|
|
43
36
|
},
|
|
44
37
|
"devDependencies": {
|
|
45
38
|
"@jest/globals": "^30.4.1",
|
|
@@ -75,8 +68,16 @@
|
|
|
75
68
|
],
|
|
76
69
|
"testEnvironment": "node",
|
|
77
70
|
"moduleNameMapper": {
|
|
71
|
+
"^@rebasepro/common$": "<rootDir>/../common/src/index.ts",
|
|
78
72
|
"^@rebasepro/types$": "<rootDir>/../types/src/index.ts",
|
|
79
73
|
"^@rebasepro/utils$": "<rootDir>/../utils/src/index.ts"
|
|
80
74
|
}
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"watch": "vite build --watch",
|
|
78
|
+
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
|
|
79
|
+
"test:lint": "eslint \"src/**\" --quiet",
|
|
80
|
+
"test": "jest --passWithNoTests --forceExit",
|
|
81
|
+
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
|
|
81
82
|
}
|
|
82
|
-
}
|
|
83
|
+
}
|
package/src/api-keys.ts
CHANGED
package/src/collection.test.ts
CHANGED
|
@@ -42,7 +42,7 @@ describe("createCollectionClient", () => {
|
|
|
42
42
|
|
|
43
43
|
const client = createCollectionClient(transport, "products");
|
|
44
44
|
const result = await client.count({
|
|
45
|
-
where: { status: "
|
|
45
|
+
where: { status: ["==", "published"] }
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
expect(transport.request).toHaveBeenCalledWith(
|
|
@@ -111,7 +111,7 @@ offset: 10 });
|
|
|
111
111
|
const client = createCollectionClient(transport, "orders");
|
|
112
112
|
await client.count({
|
|
113
113
|
where: {
|
|
114
|
-
status: "
|
|
114
|
+
status: ["==", "active"],
|
|
115
115
|
total: [">=", 100]
|
|
116
116
|
}
|
|
117
117
|
});
|
package/src/collection.ts
CHANGED
|
@@ -3,125 +3,15 @@ import { RebaseWebSocketClient } from "./websocket";
|
|
|
3
3
|
import {
|
|
4
4
|
CollectionAccessor,
|
|
5
5
|
Entity,
|
|
6
|
-
FilterOperator,
|
|
7
6
|
FilterValues,
|
|
8
7
|
FindResponse,
|
|
9
|
-
WhereFieldValue,
|
|
10
|
-
WhereFilterOp,
|
|
11
8
|
LogicalCondition,
|
|
9
|
+
WhereFilterOp,
|
|
12
10
|
WhereValue
|
|
13
11
|
} from "@rebasepro/types";
|
|
14
12
|
|
|
15
13
|
import { QueryBuilder } from "./query_builder";
|
|
16
14
|
|
|
17
|
-
function parseWhereFilter(where?: Record<string, WhereFieldValue>): FilterValues<string> | undefined {
|
|
18
|
-
if (!where) return undefined;
|
|
19
|
-
const filters: Record<string, any> = {};
|
|
20
|
-
|
|
21
|
-
const OP_TO_FILTER: Record<string, WhereFilterOp> = {
|
|
22
|
-
"eq": "==",
|
|
23
|
-
"neq": "!=",
|
|
24
|
-
"gt": ">",
|
|
25
|
-
"gte": ">=",
|
|
26
|
-
"lt": "<",
|
|
27
|
-
"lte": "<=",
|
|
28
|
-
"==": "==",
|
|
29
|
-
"!=": "!=",
|
|
30
|
-
">": ">",
|
|
31
|
-
">=": ">=",
|
|
32
|
-
"<": "<",
|
|
33
|
-
"<=": "<=",
|
|
34
|
-
"in": "in",
|
|
35
|
-
"nin": "not-in",
|
|
36
|
-
"not-in": "not-in",
|
|
37
|
-
"cs": "array-contains",
|
|
38
|
-
"csa": "array-contains-any",
|
|
39
|
-
"array-contains": "array-contains",
|
|
40
|
-
"array-contains-any": "array-contains-any"
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const parseSingle = (rawValue: any, fieldKey: string): [WhereFilterOp, unknown] => {
|
|
44
|
-
if (rawValue === null) return ["==", null];
|
|
45
|
-
if (typeof rawValue === "boolean") return ["==", rawValue];
|
|
46
|
-
if (typeof rawValue === "number") return ["==", rawValue];
|
|
47
|
-
|
|
48
|
-
if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
|
|
49
|
-
const [rawOp, val] = rawValue;
|
|
50
|
-
return [OP_TO_FILTER[rawOp] ?? "==", val];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const value = String(rawValue);
|
|
54
|
-
const dotIndex = value.indexOf(".");
|
|
55
|
-
if (dotIndex > 0) {
|
|
56
|
-
const opStr = value.substring(0, dotIndex);
|
|
57
|
-
const valStr = value.substring(dotIndex + 1);
|
|
58
|
-
let op: WhereFilterOp = "==";
|
|
59
|
-
let val: string | number | boolean | null | string[] = valStr;
|
|
60
|
-
|
|
61
|
-
switch (opStr) {
|
|
62
|
-
case "eq":
|
|
63
|
-
op = "==";
|
|
64
|
-
break;
|
|
65
|
-
case "neq":
|
|
66
|
-
op = "!=";
|
|
67
|
-
break;
|
|
68
|
-
case "gt":
|
|
69
|
-
op = ">";
|
|
70
|
-
break;
|
|
71
|
-
case "gte":
|
|
72
|
-
op = ">=";
|
|
73
|
-
break;
|
|
74
|
-
case "lt":
|
|
75
|
-
op = "<";
|
|
76
|
-
break;
|
|
77
|
-
case "lte":
|
|
78
|
-
op = "<=";
|
|
79
|
-
break;
|
|
80
|
-
case "in":
|
|
81
|
-
op = "in";
|
|
82
|
-
val = valStr.startsWith("(") && valStr.endsWith(")")
|
|
83
|
-
? valStr.slice(1, -1).split(",").map(v => v.trim())
|
|
84
|
-
: valStr.split(",");
|
|
85
|
-
break;
|
|
86
|
-
case "nin":
|
|
87
|
-
op = "not-in";
|
|
88
|
-
val = valStr.startsWith("(") && valStr.endsWith(")")
|
|
89
|
-
? valStr.slice(1, -1).split(",").map(v => v.trim())
|
|
90
|
-
: valStr.split(",");
|
|
91
|
-
break;
|
|
92
|
-
case "cs":
|
|
93
|
-
op = "array-contains";
|
|
94
|
-
break;
|
|
95
|
-
case "csa":
|
|
96
|
-
op = "array-contains-any";
|
|
97
|
-
val = valStr.startsWith("(") && valStr.endsWith(")")
|
|
98
|
-
? valStr.slice(1, -1).split(",").map(v => v.trim())
|
|
99
|
-
: valStr.split(",");
|
|
100
|
-
break;
|
|
101
|
-
default:
|
|
102
|
-
op = "==";
|
|
103
|
-
val = value;
|
|
104
|
-
}
|
|
105
|
-
if (val === "true") val = true;
|
|
106
|
-
else if (val === "false") val = false;
|
|
107
|
-
else if (val === "null") val = null;
|
|
108
|
-
else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
|
|
109
|
-
|
|
110
|
-
return [op, val];
|
|
111
|
-
} else {
|
|
112
|
-
return ["==", value];
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
for (const [key, rawValue] of Object.entries(where)) {
|
|
117
|
-
if (Array.isArray(rawValue) && rawValue.length > 0 && Array.isArray(rawValue[0])) {
|
|
118
|
-
filters[key] = rawValue.map(r => parseSingle(r, key));
|
|
119
|
-
} else {
|
|
120
|
-
filters[key] = parseSingle(rawValue, key);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return filters;
|
|
124
|
-
}
|
|
125
15
|
|
|
126
16
|
/**
|
|
127
17
|
* Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
|
|
@@ -144,10 +34,10 @@ function rowToEntity<M extends Record<string, unknown>>(row: Record<string, unkn
|
|
|
144
34
|
* Additionally it exposes fluent query builder methods like `.where()`, `.orderBy()`.
|
|
145
35
|
*/
|
|
146
36
|
export interface CollectionClient<M extends Record<string, unknown> = Record<string, unknown>> extends CollectionAccessor<M> {
|
|
147
|
-
where<K extends keyof M & string>(column: K, operator:
|
|
37
|
+
where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilder<M>;
|
|
148
38
|
where(logicalCondition: LogicalCondition): QueryBuilder<M>;
|
|
149
39
|
|
|
150
|
-
orderBy(column: keyof M & string,
|
|
40
|
+
orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilder<M>;
|
|
151
41
|
|
|
152
42
|
limit(count: number): QueryBuilder<M>;
|
|
153
43
|
|
|
@@ -227,15 +117,15 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
227
117
|
},
|
|
228
118
|
|
|
229
119
|
// Fluent builder instantiation
|
|
230
|
-
where(columnOrCondition: string | LogicalCondition, operator?:
|
|
120
|
+
where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown) {
|
|
231
121
|
const builder = new QueryBuilder<M>(client as unknown as CollectionAccessor<M>);
|
|
232
122
|
if (typeof columnOrCondition === "object") {
|
|
233
123
|
return builder.where(columnOrCondition);
|
|
234
124
|
}
|
|
235
125
|
return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
|
|
236
126
|
},
|
|
237
|
-
orderBy(column: keyof M & string,
|
|
238
|
-
return new QueryBuilder<M>(client).orderBy(column,
|
|
127
|
+
orderBy(column: keyof M & string, direction?: "asc" | "desc") {
|
|
128
|
+
return new QueryBuilder<M>(client).orderBy(column, direction);
|
|
239
129
|
},
|
|
240
130
|
limit(count: number) {
|
|
241
131
|
return new QueryBuilder<M>(client).limit(count);
|
|
@@ -253,10 +143,12 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
253
143
|
|
|
254
144
|
if (ws) {
|
|
255
145
|
client.listen = (params: FindParams | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void) => {
|
|
256
|
-
|
|
146
|
+
let active = true;
|
|
147
|
+
let lastUpdateId = 0;
|
|
148
|
+
const unsub = ws.listenCollection(
|
|
257
149
|
{
|
|
258
150
|
path: slug,
|
|
259
|
-
filter:
|
|
151
|
+
filter: params?.where,
|
|
260
152
|
limit: params?.limit,
|
|
261
153
|
startAfter: params?.offset ? String(params.offset) : undefined,
|
|
262
154
|
orderBy: params?.orderBy?.split(":")[0],
|
|
@@ -264,19 +156,49 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
264
156
|
searchString: params?.searchString
|
|
265
157
|
},
|
|
266
158
|
(entities: Entity[]) => {
|
|
159
|
+
const currentUpdateId = ++lastUpdateId;
|
|
267
160
|
const requestedLimit = params?.limit || 20;
|
|
161
|
+
const offset = params?.offset || 0;
|
|
162
|
+
|
|
163
|
+
// Immediately fire update with heuristic metadata
|
|
268
164
|
onUpdate({
|
|
269
165
|
data: entities as Entity<M>[],
|
|
270
166
|
meta: {
|
|
271
167
|
total: entities.length,
|
|
272
168
|
limit: requestedLimit,
|
|
273
|
-
offset
|
|
169
|
+
offset,
|
|
274
170
|
hasMore: entities.length >= requestedLimit
|
|
275
171
|
}
|
|
276
172
|
});
|
|
173
|
+
|
|
174
|
+
// Asynchronously fetch the actual count from the server to get accurate total/hasMore
|
|
175
|
+
if (client.count) {
|
|
176
|
+
client.count(params)
|
|
177
|
+
.then((total) => {
|
|
178
|
+
if (active && currentUpdateId === lastUpdateId) {
|
|
179
|
+
onUpdate({
|
|
180
|
+
data: entities as Entity<M>[],
|
|
181
|
+
meta: {
|
|
182
|
+
total,
|
|
183
|
+
limit: requestedLimit,
|
|
184
|
+
offset,
|
|
185
|
+
hasMore: offset + entities.length < total
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
.catch(() => {
|
|
191
|
+
// Silent fallback on count error
|
|
192
|
+
});
|
|
193
|
+
}
|
|
277
194
|
},
|
|
278
195
|
onError
|
|
279
196
|
);
|
|
197
|
+
|
|
198
|
+
return () => {
|
|
199
|
+
active = false;
|
|
200
|
+
unsub();
|
|
201
|
+
};
|
|
280
202
|
};
|
|
281
203
|
|
|
282
204
|
client.listenById = (id: string | number, onUpdate: (data: Entity<M> | undefined) => void, onError?: (error: Error) => void) => {
|
package/src/index.ts
CHANGED
|
@@ -3,11 +3,19 @@ import { createAuth, CreateAuthOptions } from "./auth";
|
|
|
3
3
|
import { createAdmin, CreateAdminOptions } from "./admin";
|
|
4
4
|
import { createCron, CreateCronOptions } from "./cron";
|
|
5
5
|
import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
|
|
6
|
-
import {
|
|
6
|
+
import { CollectionClient, createCollectionClient } from "./collection";
|
|
7
7
|
import { createFunctionsClient } from "./functions";
|
|
8
8
|
import { createStorage } from "./storage";
|
|
9
|
+
import { ClientStorageSourceRegistry } from "./storage-registry";
|
|
9
10
|
import { RebaseWebSocketClient } from "./websocket";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_STORAGE_SOURCE_KEY,
|
|
13
|
+
RebaseClient,
|
|
14
|
+
RebaseData,
|
|
15
|
+
StorageSource,
|
|
16
|
+
StorageSourceDefinition,
|
|
17
|
+
StorageSourceRegistry
|
|
18
|
+
} from "@rebasepro/types";
|
|
11
19
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
12
20
|
|
|
13
21
|
export * from "./transport";
|
|
@@ -19,6 +27,7 @@ export * from "./collection";
|
|
|
19
27
|
export * from "./query_builder";
|
|
20
28
|
export * from "./websocket";
|
|
21
29
|
export * from "./storage";
|
|
30
|
+
export * from "./storage-registry";
|
|
22
31
|
export * from "./reviver";
|
|
23
32
|
export * from "./functions";
|
|
24
33
|
export type { Entity, FindResponse } from "@rebasepro/types";
|
|
@@ -28,6 +37,21 @@ export interface CreateRebaseClientOptions extends RebaseClientConfig {
|
|
|
28
37
|
admin?: CreateAdminOptions;
|
|
29
38
|
cron?: CreateCronOptions;
|
|
30
39
|
apiKeys?: CreateApiKeysOptions;
|
|
40
|
+
/**
|
|
41
|
+
* Declared storage sources for multi-backend support. Server-transport
|
|
42
|
+
* entries are auto-wired into `client.storageRegistry`; `direct` sources
|
|
43
|
+
* are registered app-side (e.g. via a Firebase Storage hook). The default
|
|
44
|
+
* source (`storage`) is always registered under
|
|
45
|
+
* {@link DEFAULT_STORAGE_SOURCE_KEY}.
|
|
46
|
+
*/
|
|
47
|
+
storageSources?: StorageSourceDefinition[];
|
|
48
|
+
/**
|
|
49
|
+
* Maps camelCase property names / safe identifiers to the actual
|
|
50
|
+
* collection slugs on the server (e.g. `{ companyMembers: "company-members" }`).
|
|
51
|
+
* If provided, the data layer proxy will resolve property accessors to their
|
|
52
|
+
* correct slugs via this map before falling back to automatic snake_casing.
|
|
53
|
+
*/
|
|
54
|
+
collections?: Record<string, string>;
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
// ─── Typed Data Proxy ────────────────────────────────────────────────────────
|
|
@@ -69,6 +93,9 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
69
93
|
functions: ReturnType<typeof createFunctionsClient>;
|
|
70
94
|
ws?: RebaseWebSocketClient;
|
|
71
95
|
storage: StorageSource;
|
|
96
|
+
storageRegistry: StorageSourceRegistry;
|
|
97
|
+
createStorageSource: (storageId: string) => StorageSource;
|
|
98
|
+
fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
|
|
72
99
|
call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
|
|
73
100
|
data: TypedDataLayer<DB>;
|
|
74
101
|
};
|
|
@@ -118,6 +145,48 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
118
145
|
const storage = createStorage(transport);
|
|
119
146
|
const functions = createFunctionsClient(transport);
|
|
120
147
|
|
|
148
|
+
// Build a server-backed StorageSource for a given storage-source key.
|
|
149
|
+
const createStorageSource = (storageId: string): StorageSource =>
|
|
150
|
+
storageId === DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
|
|
151
|
+
|
|
152
|
+
// Storage registry: always holds the default source, plus any declared
|
|
153
|
+
// server-transport sources. `direct` sources are registered app-side.
|
|
154
|
+
const storageRegistry = new ClientStorageSourceRegistry();
|
|
155
|
+
storageRegistry.register(DEFAULT_STORAGE_SOURCE_KEY, storage);
|
|
156
|
+
for (const def of options.storageSources ?? []) {
|
|
157
|
+
if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY) {
|
|
158
|
+
storageRegistry.register(def.key, createStorageSource(def.key));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Discover storage sources from the backend, making the server the single
|
|
163
|
+
// source of truth. Server-transport sources are auto-wired into the
|
|
164
|
+
// registry; `direct` sources are returned for the app to register. The
|
|
165
|
+
// promise is cached on success and reset on failure so it can be retried
|
|
166
|
+
// (e.g. once the user authenticates).
|
|
167
|
+
let storageSourcesPromise: Promise<StorageSourceDefinition[]> | undefined;
|
|
168
|
+
const fetchStorageSources = (): Promise<StorageSourceDefinition[]> => {
|
|
169
|
+
if (storageSourcesPromise) return storageSourcesPromise;
|
|
170
|
+
storageSourcesPromise = transport
|
|
171
|
+
.request<{ data: StorageSourceDefinition[] }>("/storage/sources")
|
|
172
|
+
.then((res) => {
|
|
173
|
+
const defs = res.data ?? [];
|
|
174
|
+
for (const def of defs) {
|
|
175
|
+
if (def.transport === "server"
|
|
176
|
+
&& def.key !== DEFAULT_STORAGE_SOURCE_KEY
|
|
177
|
+
&& !storageRegistry.has(def.key)) {
|
|
178
|
+
storageRegistry.register(def.key, createStorageSource(def.key));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return defs;
|
|
182
|
+
})
|
|
183
|
+
.catch((e) => {
|
|
184
|
+
storageSourcesPromise = undefined; // allow retry
|
|
185
|
+
throw e;
|
|
186
|
+
});
|
|
187
|
+
return storageSourcesPromise;
|
|
188
|
+
};
|
|
189
|
+
|
|
121
190
|
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
122
191
|
|
|
123
192
|
let ws: RebaseWebSocketClient | undefined;
|
|
@@ -190,6 +259,9 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
190
259
|
}
|
|
191
260
|
if (typeof prop === "symbol") return undefined;
|
|
192
261
|
if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
|
|
262
|
+
if (options.collections && prop in options.collections) {
|
|
263
|
+
return collection(options.collections[prop]);
|
|
264
|
+
}
|
|
193
265
|
// Convert camelCase property names to snake_case slugs.
|
|
194
266
|
// e.g. `companyMembers` → `company_members`
|
|
195
267
|
const slug = toSnakeCase(prop);
|
|
@@ -206,6 +278,9 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
206
278
|
apiKeys,
|
|
207
279
|
functions,
|
|
208
280
|
storage,
|
|
281
|
+
storageRegistry,
|
|
282
|
+
createStorageSource,
|
|
283
|
+
fetchStorageSources,
|
|
209
284
|
ws,
|
|
210
285
|
setToken: transport.setToken,
|
|
211
286
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
@@ -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
|
+
}
|