@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rebase
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -10,6 +10,7 @@ export interface ApiKeyMasked {
10
10
  name: string;
11
11
  key_prefix: string;
12
12
  permissions: ApiKeyPermission[];
13
+ admin: boolean;
13
14
  rate_limit: number | null;
14
15
  created_by: string;
15
16
  created_at: string;
@@ -1,6 +1,6 @@
1
1
  import { FindParams, Transport } from "./transport";
2
2
  import { RebaseWebSocketClient } from "./websocket";
3
- import { CollectionAccessor, FilterOperator, LogicalCondition, WhereValue } from "@rebasepro/types";
3
+ import { CollectionAccessor, LogicalCondition, WhereFilterOp, WhereValue } from "@rebasepro/types";
4
4
  import { QueryBuilder } from "./query_builder";
5
5
  /**
6
6
  * CollectionClient extends `CollectionAccessor` from `@rebasepro/types` so that
@@ -9,9 +9,9 @@ import { QueryBuilder } from "./query_builder";
9
9
  * Additionally it exposes fluent query builder methods like `.where()`, `.orderBy()`.
10
10
  */
11
11
  export interface CollectionClient<M extends Record<string, unknown> = Record<string, unknown>> extends CollectionAccessor<M> {
12
- where<K extends keyof M & string>(column: K, operator: FilterOperator, value: WhereValue<M[K]>): QueryBuilder<M>;
12
+ where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilder<M>;
13
13
  where(logicalCondition: LogicalCondition): QueryBuilder<M>;
14
- orderBy(column: keyof M & string, ascending?: "asc" | "desc"): QueryBuilder<M>;
14
+ orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilder<M>;
15
15
  limit(count: number): QueryBuilder<M>;
16
16
  offset(count: number): QueryBuilder<M>;
17
17
  search(searchString: string): QueryBuilder<M>;
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
6
6
  import { CollectionClient } from "./collection";
7
7
  import { createFunctionsClient } from "./functions";
8
8
  import { RebaseWebSocketClient } from "./websocket";
9
- import { RebaseClient, RebaseData, StorageSource } from "@rebasepro/types";
9
+ import { RebaseClient, RebaseData, StorageSource, StorageSourceDefinition, StorageSourceRegistry } from "@rebasepro/types";
10
10
  export * from "./transport";
11
11
  export * from "./auth";
12
12
  export * from "./admin";
@@ -16,6 +16,7 @@ export * from "./collection";
16
16
  export * from "./query_builder";
17
17
  export * from "./websocket";
18
18
  export * from "./storage";
19
+ export * from "./storage-registry";
19
20
  export * from "./reviver";
20
21
  export * from "./functions";
21
22
  export type { Entity, FindResponse } from "@rebasepro/types";
@@ -24,6 +25,21 @@ export interface CreateRebaseClientOptions extends RebaseClientConfig {
24
25
  admin?: CreateAdminOptions;
25
26
  cron?: CreateCronOptions;
26
27
  apiKeys?: CreateApiKeysOptions;
28
+ /**
29
+ * Declared storage sources for multi-backend support. Server-transport
30
+ * entries are auto-wired into `client.storageRegistry`; `direct` sources
31
+ * are registered app-side (e.g. via a Firebase Storage hook). The default
32
+ * source (`storage`) is always registered under
33
+ * {@link DEFAULT_STORAGE_SOURCE_KEY}.
34
+ */
35
+ storageSources?: StorageSourceDefinition[];
36
+ /**
37
+ * Maps camelCase property names / safe identifiers to the actual
38
+ * collection slugs on the server (e.g. `{ companyMembers: "company-members" }`).
39
+ * If provided, the data layer proxy will resolve property accessors to their
40
+ * correct slugs via this map before falling back to automatic snake_casing.
41
+ */
42
+ collections?: Record<string, string>;
27
43
  }
28
44
  type KebabToCamelCase<S extends string> = S extends `${infer T}-${infer U}` ? `${T}${Capitalize<KebabToCamelCase<U>>}` : S;
29
45
  type TypedDataLayer<DB> = {
@@ -54,6 +70,9 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
54
70
  functions: ReturnType<typeof createFunctionsClient>;
55
71
  ws?: RebaseWebSocketClient;
56
72
  storage: StorageSource;
73
+ storageRegistry: StorageSourceRegistry;
74
+ createStorageSource: (storageId: string) => StorageSource;
75
+ fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
57
76
  call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
58
77
  data: TypedDataLayer<DB>;
59
78
  };
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
- import { EntityReference, EntityRelation, GeoPoint, Vector } from "@rebasepro/types";
2
- import { QueryBuilder, and, cond, or } from "@rebasepro/common";
1
+ import { QueryBuilder, and, cond, or, serializeFilter, serializeLogicalCondition } from "@rebasepro/common";
2
+ import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, Vector } from "@rebasepro/types";
3
3
  import { toSnakeCase } from "@rebasepro/utils";
4
4
  //#region src/reviver.ts
5
5
  function rebaseReviver(_key, value) {
@@ -42,61 +42,6 @@ var RebaseApiError = class extends Error {
42
42
  this.details = details;
43
43
  }
44
44
  };
45
- /**
46
- * Maps a short operator alias to the PostgREST-style short code.
47
- */
48
- var OP_MAP = {
49
- "==": "eq",
50
- "!=": "neq",
51
- ">": "gt",
52
- ">=": "gte",
53
- "<": "lt",
54
- "<=": "lte",
55
- "not-in": "nin",
56
- "array-contains": "cs",
57
- "array-contains-any": "csa"
58
- };
59
- /**
60
- * Normalise a single `WhereFieldValue` into the PostgREST query-string
61
- * representation the backend expects.
62
- *
63
- * Supports:
64
- * - `null` → `"eq.null"`
65
- * - `true`/`false` → `"eq.true"` / `"eq.false"`
66
- * - `42` → `"42"` (plain equality)
67
- * - `"active"` → `"active"` (plain equality, backward-compat)
68
- * - `"gte.18"` → `"gte.18"` (pass-through PostgREST string)
69
- * - `[">=", 18]` → `"gte.18"` (tuple syntax)
70
- * - `["in", [1,2]]` → `"in.(1,2)"` (tuple with array value)
71
- * - `["!=", null]` → `"neq.null"`
72
- */
73
- function normalizeWhereValue(value) {
74
- if (value === null) return "eq.null";
75
- if (typeof value === "boolean") return `eq.${value}`;
76
- if (typeof value === "number") return String(value);
77
- if (Array.isArray(value)) {
78
- const [rawOp, val] = (Array.isArray(value[0]) ? value : [value])[0] || [];
79
- if (rawOp) {
80
- const op = OP_MAP[rawOp] ?? rawOp;
81
- if (val === null) return `${op}.null`;
82
- if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
83
- return `${op}.${val}`;
84
- }
85
- }
86
- return String(value);
87
- }
88
- function serializeLogicalCondition(cond) {
89
- if ("type" in cond) {
90
- const sub = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
91
- return `${cond.type}(${sub})`;
92
- } else {
93
- const op = OP_MAP[cond.operator] ?? cond.operator;
94
- let formattedValue = cond.value;
95
- if (Array.isArray(cond.value)) formattedValue = `(${cond.value.join(",")})`;
96
- else if (cond.value === null) formattedValue = "null";
97
- return `${cond.column}.${op}.${formattedValue}`;
98
- }
99
- }
100
45
  function buildQueryString(params) {
101
46
  if (!params) return "";
102
47
  const parts = [];
@@ -111,13 +56,10 @@ function buildQueryString(params) {
111
56
  const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
112
57
  parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
113
58
  }
114
- if (params.where) for (const [field, value] of Object.entries(params.where)) if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) for (const subVal of value) {
115
- const normalized = normalizeWhereValue(subVal);
116
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
117
- }
118
- else {
119
- const normalized = normalizeWhereValue(value);
120
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
59
+ if (params.where) {
60
+ const serialized = serializeFilter(params.where);
61
+ for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
62
+ else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
121
63
  }
122
64
  return parts.length > 0 ? "?" + parts.join("&") : "";
123
65
  }
@@ -869,94 +811,6 @@ function createApiKeys(transport, options) {
869
811
  }
870
812
  //#endregion
871
813
  //#region src/collection.ts
872
- function parseWhereFilter(where) {
873
- if (!where) return void 0;
874
- const filters = {};
875
- const OP_TO_FILTER = {
876
- "eq": "==",
877
- "neq": "!=",
878
- "gt": ">",
879
- "gte": ">=",
880
- "lt": "<",
881
- "lte": "<=",
882
- "==": "==",
883
- "!=": "!=",
884
- ">": ">",
885
- ">=": ">=",
886
- "<": "<",
887
- "<=": "<=",
888
- "in": "in",
889
- "nin": "not-in",
890
- "not-in": "not-in",
891
- "cs": "array-contains",
892
- "csa": "array-contains-any",
893
- "array-contains": "array-contains",
894
- "array-contains-any": "array-contains-any"
895
- };
896
- const parseSingle = (rawValue, fieldKey) => {
897
- if (rawValue === null) return ["==", null];
898
- if (typeof rawValue === "boolean") return ["==", rawValue];
899
- if (typeof rawValue === "number") return ["==", rawValue];
900
- if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
901
- const [rawOp, val] = rawValue;
902
- return [OP_TO_FILTER[rawOp] ?? "==", val];
903
- }
904
- const value = String(rawValue);
905
- const dotIndex = value.indexOf(".");
906
- if (dotIndex > 0) {
907
- const opStr = value.substring(0, dotIndex);
908
- const valStr = value.substring(dotIndex + 1);
909
- let op = "==";
910
- let val = valStr;
911
- switch (opStr) {
912
- case "eq":
913
- op = "==";
914
- break;
915
- case "neq":
916
- op = "!=";
917
- break;
918
- case "gt":
919
- op = ">";
920
- break;
921
- case "gte":
922
- op = ">=";
923
- break;
924
- case "lt":
925
- op = "<";
926
- break;
927
- case "lte":
928
- op = "<=";
929
- break;
930
- case "in":
931
- op = "in";
932
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
933
- break;
934
- case "nin":
935
- op = "not-in";
936
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
937
- break;
938
- case "cs":
939
- op = "array-contains";
940
- break;
941
- case "csa":
942
- op = "array-contains-any";
943
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
944
- break;
945
- default:
946
- op = "==";
947
- val = value;
948
- }
949
- if (val === "true") val = true;
950
- else if (val === "false") val = false;
951
- else if (val === "null") val = null;
952
- else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
953
- return [op, val];
954
- } else return ["==", value];
955
- };
956
- for (const [key, rawValue] of Object.entries(where)) if (Array.isArray(rawValue) && rawValue.length > 0 && Array.isArray(rawValue[0])) filters[key] = rawValue.map((r) => parseSingle(r, key));
957
- else filters[key] = parseSingle(rawValue, key);
958
- return filters;
959
- }
960
814
  /**
961
815
  * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
962
816
  * a proper `Entity<M>` structure expected by the core framework.
@@ -1021,8 +875,8 @@ function createCollectionClient(transport, slug, ws) {
1021
875
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
1022
876
  return builder.where(columnOrCondition, operator, value);
1023
877
  },
1024
- orderBy(column, ascending) {
1025
- return new QueryBuilder(client).orderBy(column, ascending);
878
+ orderBy(column, direction) {
879
+ return new QueryBuilder(client).orderBy(column, direction);
1026
880
  },
1027
881
  limit(count) {
1028
882
  return new QueryBuilder(client).limit(count);
@@ -1039,26 +893,45 @@ function createCollectionClient(transport, slug, ws) {
1039
893
  };
1040
894
  if (ws) {
1041
895
  client.listen = (params, onUpdate, onError) => {
1042
- return ws.listenCollection({
896
+ let active = true;
897
+ let lastUpdateId = 0;
898
+ const unsub = ws.listenCollection({
1043
899
  path: slug,
1044
- filter: parseWhereFilter(params?.where),
900
+ filter: params?.where,
1045
901
  limit: params?.limit,
1046
902
  startAfter: params?.offset ? String(params.offset) : void 0,
1047
903
  orderBy: params?.orderBy?.split(":")[0],
1048
904
  order: params?.orderBy?.split(":")[1],
1049
905
  searchString: params?.searchString
1050
906
  }, (entities) => {
907
+ const currentUpdateId = ++lastUpdateId;
1051
908
  const requestedLimit = params?.limit || 20;
909
+ const offset = params?.offset || 0;
1052
910
  onUpdate({
1053
911
  data: entities,
1054
912
  meta: {
1055
913
  total: entities.length,
1056
914
  limit: requestedLimit,
1057
- offset: params?.offset || 0,
915
+ offset,
1058
916
  hasMore: entities.length >= requestedLimit
1059
917
  }
1060
918
  });
919
+ if (client.count) client.count(params).then((total) => {
920
+ if (active && currentUpdateId === lastUpdateId) onUpdate({
921
+ data: entities,
922
+ meta: {
923
+ total,
924
+ limit: requestedLimit,
925
+ offset,
926
+ hasMore: offset + entities.length < total
927
+ }
928
+ });
929
+ }).catch(() => {});
1061
930
  }, onError);
931
+ return () => {
932
+ active = false;
933
+ unsub();
934
+ };
1062
935
  };
1063
936
  client.listenById = (id, onUpdate, onError) => {
1064
937
  return ws.listenEntity({
@@ -1098,17 +971,31 @@ function createFunctionsClient(transport) {
1098
971
  }
1099
972
  //#endregion
1100
973
  //#region src/storage.ts
1101
- function createStorage(transport) {
974
+ /**
975
+ * Create a StorageSource that talks to the Rebase backend REST API.
976
+ *
977
+ * @param transport - HTTP transport instance
978
+ * @param storageId - Optional storage-source key for multi-backend routing.
979
+ * When set, it is forwarded to the server so the correct
980
+ * `StorageController` is resolved from the registry.
981
+ */
982
+ function createStorage(transport, storageId) {
1102
983
  const urlsCache = /* @__PURE__ */ new Map();
984
+ /** Append ?storageId=... to a path when multi-backend routing is active. */
985
+ const withStorageId = (path) => {
986
+ if (!storageId) return path;
987
+ return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
988
+ };
1103
989
  async function putObject({ file, key, metadata, bucket }) {
1104
990
  const formData = new FormData();
1105
991
  formData.append("file", file);
1106
992
  if (key) formData.append("key", key);
1107
993
  if (bucket) formData.append("bucket", bucket);
994
+ if (storageId) formData.append("storageId", storageId);
1108
995
  if (metadata) {
1109
996
  for (const [key, value] of Object.entries(metadata)) if (value !== void 0 && value !== null) formData.append(`metadata_${key}`, typeof value === "string" ? value : JSON.stringify(value));
1110
997
  }
1111
- return (await transport.request("/storage/upload", {
998
+ return (await transport.request(withStorageId("/storage/upload"), {
1112
999
  method: "POST",
1113
1000
  body: formData,
1114
1001
  headers: {}
@@ -1119,18 +1006,18 @@ function createStorage(transport) {
1119
1006
  const cached = urlsCache.get(cacheKey);
1120
1007
  if (cached) return cached;
1121
1008
  let filePath = keyOrUrl;
1122
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1009
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1123
1010
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1124
1011
  if (!filePath || filePath.trim() === "" || filePath === "/") return {
1125
1012
  url: null,
1126
1013
  fileNotFound: true
1127
1014
  };
1128
1015
  try {
1129
- const result = await transport.request(`/storage/metadata/${filePath}`);
1016
+ const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
1130
1017
  const activeToken = await transport.resolveToken();
1131
1018
  const tokenQuery = activeToken ? `?token=${activeToken}` : "";
1132
1019
  const downloadConfig = {
1133
- url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
1020
+ url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
1134
1021
  metadata: result.data
1135
1022
  };
1136
1023
  urlsCache.set(cacheKey, downloadConfig);
@@ -1145,10 +1032,10 @@ function createStorage(transport) {
1145
1032
  }
1146
1033
  async function getObject(key, bucket) {
1147
1034
  let filePath = key;
1148
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1035
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1149
1036
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1150
1037
  if (!filePath || filePath.trim() === "" || filePath === "/") return null;
1151
- const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
1038
+ const url = withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`);
1152
1039
  const response = await transport.fetchFn(url, { headers: transport.getHeaders ? transport.getHeaders() : {} });
1153
1040
  if (response.status === 404) return null;
1154
1041
  if (!response.ok) throw new Error("Failed to get file");
@@ -1158,11 +1045,11 @@ function createStorage(transport) {
1158
1045
  }
1159
1046
  async function deleteObject(key, bucket) {
1160
1047
  let filePath = key;
1161
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1048
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1162
1049
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1163
1050
  if (!filePath || filePath.trim() === "" || filePath === "/") return;
1164
1051
  try {
1165
- await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
1052
+ await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
1166
1053
  } catch (e) {
1167
1054
  if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
1168
1055
  }
@@ -1174,6 +1061,7 @@ function createStorage(transport) {
1174
1061
  if (options?.bucket) params.set("bucket", options.bucket);
1175
1062
  if (options?.maxResults) params.set("maxResults", String(options.maxResults));
1176
1063
  if (options?.pageToken) params.set("pageToken", options.pageToken);
1064
+ if (storageId) params.set("storageId", storageId);
1177
1065
  return (await transport.request(`/storage/list?${params.toString()}`)).data;
1178
1066
  }
1179
1067
  return {
@@ -1185,6 +1073,62 @@ function createStorage(transport) {
1185
1073
  };
1186
1074
  }
1187
1075
  //#endregion
1076
+ //#region src/storage-registry.ts
1077
+ /**
1078
+ * Default implementation of the client-side `StorageSourceRegistry`.
1079
+ */
1080
+ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
1081
+ sources = /* @__PURE__ */ new Map();
1082
+ /**
1083
+ * Register a storage source.
1084
+ * @param key - Unique key matching a `StorageSourceDefinition.key`
1085
+ * @param source - The `StorageSource` instance
1086
+ */
1087
+ register(key, source) {
1088
+ this.sources.set(key, source);
1089
+ }
1090
+ getDefault() {
1091
+ const source = this.sources.get(DEFAULT_STORAGE_SOURCE_KEY);
1092
+ if (!source) throw new Error(`[StorageSourceRegistry] No default storage source registered. Register one with key "${DEFAULT_STORAGE_SOURCE_KEY}".`);
1093
+ return source;
1094
+ }
1095
+ get(key) {
1096
+ if (key === void 0 || key === null) return this.sources.get(DEFAULT_STORAGE_SOURCE_KEY);
1097
+ return this.sources.get(key);
1098
+ }
1099
+ getOrDefault(key) {
1100
+ if (key === void 0 || key === null) return this.getDefault();
1101
+ const source = this.sources.get(key);
1102
+ if (source) return source;
1103
+ console.warn(`[StorageSourceRegistry] Storage source "${key}" not found, falling back to "${DEFAULT_STORAGE_SOURCE_KEY}".`);
1104
+ return this.getDefault();
1105
+ }
1106
+ has(key) {
1107
+ return this.sources.has(key);
1108
+ }
1109
+ list() {
1110
+ return Array.from(this.sources.keys());
1111
+ }
1112
+ /**
1113
+ * Build a registry from `StorageSourceDefinition[]` and an HTTP transport.
1114
+ *
1115
+ * - Sources with `transport: "server"` are auto-wired via `createStorage(transport, key)`.
1116
+ * - Sources with `transport: "direct"` are **not** auto-wired — they must
1117
+ * be registered manually after this call (e.g. via a Firebase hook).
1118
+ *
1119
+ * @param definitions - Array of storage source definitions
1120
+ * @param transport - HTTP transport for server-backed sources
1121
+ */
1122
+ static fromDefinitions(definitions, transport) {
1123
+ const registry = new ClientStorageSourceRegistry();
1124
+ for (const def of definitions) if (def.transport === "server") {
1125
+ const source = createStorage(transport, def.key === DEFAULT_STORAGE_SOURCE_KEY ? void 0 : def.key);
1126
+ registry.register(def.key, source);
1127
+ }
1128
+ return registry;
1129
+ }
1130
+ };
1131
+ //#endregion
1188
1132
  //#region src/websocket.ts
1189
1133
  /**
1190
1134
  * Extract error message and code from a WebSocket message payload.
@@ -2122,6 +2066,23 @@ function createRebaseClient(options) {
2122
2066
  const apiKeys = createApiKeys(transport, options.apiKeys);
2123
2067
  const storage = createStorage(transport);
2124
2068
  const functions = createFunctionsClient(transport);
2069
+ const createStorageSource = (storageId) => storageId === DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
2070
+ const storageRegistry = new ClientStorageSourceRegistry();
2071
+ storageRegistry.register(DEFAULT_STORAGE_SOURCE_KEY, storage);
2072
+ for (const def of options.storageSources ?? []) if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY) storageRegistry.register(def.key, createStorageSource(def.key));
2073
+ let storageSourcesPromise;
2074
+ const fetchStorageSources = () => {
2075
+ if (storageSourcesPromise) return storageSourcesPromise;
2076
+ storageSourcesPromise = transport.request("/storage/sources").then((res) => {
2077
+ const defs = res.data ?? [];
2078
+ for (const def of defs) if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY && !storageRegistry.has(def.key)) storageRegistry.register(def.key, createStorageSource(def.key));
2079
+ return defs;
2080
+ }).catch((e) => {
2081
+ storageSourcesPromise = void 0;
2082
+ throw e;
2083
+ });
2084
+ return storageSourcesPromise;
2085
+ };
2125
2086
  const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
2126
2087
  let ws;
2127
2088
  if (resolvedWsUrl) {
@@ -2167,7 +2128,10 @@ function createRebaseClient(options) {
2167
2128
  const dataProxy = new Proxy({ collection }, { get(_target, prop) {
2168
2129
  if (prop === "collection") return collection;
2169
2130
  if (typeof prop === "symbol") return void 0;
2170
- if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") return collection(toSnakeCase(prop));
2131
+ if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
2132
+ if (options.collections && prop in options.collections) return collection(options.collections[prop]);
2133
+ return collection(toSnakeCase(prop));
2134
+ }
2171
2135
  } });
2172
2136
  return {
2173
2137
  auth,
@@ -2176,6 +2140,9 @@ function createRebaseClient(options) {
2176
2140
  apiKeys,
2177
2141
  functions,
2178
2142
  storage,
2143
+ storageRegistry,
2144
+ createStorageSource,
2145
+ fetchStorageSources,
2179
2146
  ws,
2180
2147
  setToken: transport.setToken,
2181
2148
  setAuthTokenGetter: transport.setAuthTokenGetter,
@@ -2196,6 +2163,6 @@ function createRebaseClient(options) {
2196
2163
  };
2197
2164
  }
2198
2165
  //#endregion
2199
- export { ApiError, QueryBuilder, RebaseApiError, RebaseWebSocketClient, and, buildQueryString, cond, createAdmin, createApiKeys, createAuth, createCollectionClient, createCookieStorage, createCron, createFunctionsClient, createMemoryStorage, createRebaseClient, createStorage, createTransport, or, rebaseReviver };
2166
+ export { ApiError, ClientStorageSourceRegistry, QueryBuilder, RebaseApiError, RebaseWebSocketClient, and, buildQueryString, cond, createAdmin, createApiKeys, createAuth, createCollectionClient, createCookieStorage, createCron, createFunctionsClient, createMemoryStorage, createRebaseClient, createStorage, createTransport, or, rebaseReviver };
2200
2167
 
2201
2168
  //# sourceMappingURL=index.es.js.map