@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/dist/index.umd.js CHANGED
@@ -1,11 +1,11 @@
1
1
  (function(global, factory) {
2
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@rebasepro/types"), require("@rebasepro/common"), require("@rebasepro/utils")) : typeof define === "function" && define.amd ? define([
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@rebasepro/common"), require("@rebasepro/types"), require("@rebasepro/utils")) : typeof define === "function" && define.amd ? define([
3
3
  "exports",
4
- "@rebasepro/types",
5
4
  "@rebasepro/common",
5
+ "@rebasepro/types",
6
6
  "@rebasepro/utils"
7
- ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Client"] = {}, global._rebasepro_types, global._rebasepro_common, global._rebasepro_utils));
8
- })(this, function(exports, _rebasepro_types, _rebasepro_common, _rebasepro_utils) {
7
+ ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase Client"] = {}, global._rebasepro_common, global._rebasepro_types, global._rebasepro_utils));
8
+ })(this, function(exports, _rebasepro_common, _rebasepro_types, _rebasepro_utils) {
9
9
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
10
10
  //#region src/reviver.ts
11
11
  function rebaseReviver(_key, value) {
@@ -48,61 +48,6 @@
48
48
  this.details = details;
49
49
  }
50
50
  };
51
- /**
52
- * Maps a short operator alias to the PostgREST-style short code.
53
- */
54
- var OP_MAP = {
55
- "==": "eq",
56
- "!=": "neq",
57
- ">": "gt",
58
- ">=": "gte",
59
- "<": "lt",
60
- "<=": "lte",
61
- "not-in": "nin",
62
- "array-contains": "cs",
63
- "array-contains-any": "csa"
64
- };
65
- /**
66
- * Normalise a single `WhereFieldValue` into the PostgREST query-string
67
- * representation the backend expects.
68
- *
69
- * Supports:
70
- * - `null` → `"eq.null"`
71
- * - `true`/`false` → `"eq.true"` / `"eq.false"`
72
- * - `42` → `"42"` (plain equality)
73
- * - `"active"` → `"active"` (plain equality, backward-compat)
74
- * - `"gte.18"` → `"gte.18"` (pass-through PostgREST string)
75
- * - `[">=", 18]` → `"gte.18"` (tuple syntax)
76
- * - `["in", [1,2]]` → `"in.(1,2)"` (tuple with array value)
77
- * - `["!=", null]` → `"neq.null"`
78
- */
79
- function normalizeWhereValue(value) {
80
- if (value === null) return "eq.null";
81
- if (typeof value === "boolean") return `eq.${value}`;
82
- if (typeof value === "number") return String(value);
83
- if (Array.isArray(value)) {
84
- const [rawOp, val] = (Array.isArray(value[0]) ? value : [value])[0] || [];
85
- if (rawOp) {
86
- const op = OP_MAP[rawOp] ?? rawOp;
87
- if (val === null) return `${op}.null`;
88
- if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
89
- return `${op}.${val}`;
90
- }
91
- }
92
- return String(value);
93
- }
94
- function serializeLogicalCondition(cond) {
95
- if ("type" in cond) {
96
- const sub = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
97
- return `${cond.type}(${sub})`;
98
- } else {
99
- const op = OP_MAP[cond.operator] ?? cond.operator;
100
- let formattedValue = cond.value;
101
- if (Array.isArray(cond.value)) formattedValue = `(${cond.value.join(",")})`;
102
- else if (cond.value === null) formattedValue = "null";
103
- return `${cond.column}.${op}.${formattedValue}`;
104
- }
105
- }
106
51
  function buildQueryString(params) {
107
52
  if (!params) return "";
108
53
  const parts = [];
@@ -114,16 +59,13 @@
114
59
  if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
115
60
  if (params.logical) {
116
61
  const root = params.logical;
117
- const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
62
+ const serialized = (root.conditions ?? []).map(_rebasepro_common.serializeLogicalCondition).join(",");
118
63
  parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
119
64
  }
120
- 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) {
121
- const normalized = normalizeWhereValue(subVal);
122
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
123
- }
124
- else {
125
- const normalized = normalizeWhereValue(value);
126
- parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
65
+ if (params.where) {
66
+ const serialized = (0, _rebasepro_common.serializeFilter)(params.where);
67
+ for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
68
+ else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
127
69
  }
128
70
  return parts.length > 0 ? "?" + parts.join("&") : "";
129
71
  }
@@ -875,94 +817,6 @@
875
817
  }
876
818
  //#endregion
877
819
  //#region src/collection.ts
878
- function parseWhereFilter(where) {
879
- if (!where) return void 0;
880
- const filters = {};
881
- const OP_TO_FILTER = {
882
- "eq": "==",
883
- "neq": "!=",
884
- "gt": ">",
885
- "gte": ">=",
886
- "lt": "<",
887
- "lte": "<=",
888
- "==": "==",
889
- "!=": "!=",
890
- ">": ">",
891
- ">=": ">=",
892
- "<": "<",
893
- "<=": "<=",
894
- "in": "in",
895
- "nin": "not-in",
896
- "not-in": "not-in",
897
- "cs": "array-contains",
898
- "csa": "array-contains-any",
899
- "array-contains": "array-contains",
900
- "array-contains-any": "array-contains-any"
901
- };
902
- const parseSingle = (rawValue, fieldKey) => {
903
- if (rawValue === null) return ["==", null];
904
- if (typeof rawValue === "boolean") return ["==", rawValue];
905
- if (typeof rawValue === "number") return ["==", rawValue];
906
- if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
907
- const [rawOp, val] = rawValue;
908
- return [OP_TO_FILTER[rawOp] ?? "==", val];
909
- }
910
- const value = String(rawValue);
911
- const dotIndex = value.indexOf(".");
912
- if (dotIndex > 0) {
913
- const opStr = value.substring(0, dotIndex);
914
- const valStr = value.substring(dotIndex + 1);
915
- let op = "==";
916
- let val = valStr;
917
- switch (opStr) {
918
- case "eq":
919
- op = "==";
920
- break;
921
- case "neq":
922
- op = "!=";
923
- break;
924
- case "gt":
925
- op = ">";
926
- break;
927
- case "gte":
928
- op = ">=";
929
- break;
930
- case "lt":
931
- op = "<";
932
- break;
933
- case "lte":
934
- op = "<=";
935
- break;
936
- case "in":
937
- op = "in";
938
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
939
- break;
940
- case "nin":
941
- op = "not-in";
942
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
943
- break;
944
- case "cs":
945
- op = "array-contains";
946
- break;
947
- case "csa":
948
- op = "array-contains-any";
949
- val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
950
- break;
951
- default:
952
- op = "==";
953
- val = value;
954
- }
955
- if (val === "true") val = true;
956
- else if (val === "false") val = false;
957
- else if (val === "null") val = null;
958
- else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
959
- return [op, val];
960
- } else return ["==", value];
961
- };
962
- 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));
963
- else filters[key] = parseSingle(rawValue, key);
964
- return filters;
965
- }
966
820
  /**
967
821
  * Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
968
822
  * a proper `Entity<M>` structure expected by the core framework.
@@ -1027,8 +881,8 @@
1027
881
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
1028
882
  return builder.where(columnOrCondition, operator, value);
1029
883
  },
1030
- orderBy(column, ascending) {
1031
- return new _rebasepro_common.QueryBuilder(client).orderBy(column, ascending);
884
+ orderBy(column, direction) {
885
+ return new _rebasepro_common.QueryBuilder(client).orderBy(column, direction);
1032
886
  },
1033
887
  limit(count) {
1034
888
  return new _rebasepro_common.QueryBuilder(client).limit(count);
@@ -1045,26 +899,45 @@
1045
899
  };
1046
900
  if (ws) {
1047
901
  client.listen = (params, onUpdate, onError) => {
1048
- return ws.listenCollection({
902
+ let active = true;
903
+ let lastUpdateId = 0;
904
+ const unsub = ws.listenCollection({
1049
905
  path: slug,
1050
- filter: parseWhereFilter(params?.where),
906
+ filter: params?.where,
1051
907
  limit: params?.limit,
1052
908
  startAfter: params?.offset ? String(params.offset) : void 0,
1053
909
  orderBy: params?.orderBy?.split(":")[0],
1054
910
  order: params?.orderBy?.split(":")[1],
1055
911
  searchString: params?.searchString
1056
912
  }, (entities) => {
913
+ const currentUpdateId = ++lastUpdateId;
1057
914
  const requestedLimit = params?.limit || 20;
915
+ const offset = params?.offset || 0;
1058
916
  onUpdate({
1059
917
  data: entities,
1060
918
  meta: {
1061
919
  total: entities.length,
1062
920
  limit: requestedLimit,
1063
- offset: params?.offset || 0,
921
+ offset,
1064
922
  hasMore: entities.length >= requestedLimit
1065
923
  }
1066
924
  });
925
+ if (client.count) client.count(params).then((total) => {
926
+ if (active && currentUpdateId === lastUpdateId) onUpdate({
927
+ data: entities,
928
+ meta: {
929
+ total,
930
+ limit: requestedLimit,
931
+ offset,
932
+ hasMore: offset + entities.length < total
933
+ }
934
+ });
935
+ }).catch(() => {});
1067
936
  }, onError);
937
+ return () => {
938
+ active = false;
939
+ unsub();
940
+ };
1068
941
  };
1069
942
  client.listenById = (id, onUpdate, onError) => {
1070
943
  return ws.listenEntity({
@@ -1104,17 +977,31 @@
1104
977
  }
1105
978
  //#endregion
1106
979
  //#region src/storage.ts
1107
- function createStorage(transport) {
980
+ /**
981
+ * Create a StorageSource that talks to the Rebase backend REST API.
982
+ *
983
+ * @param transport - HTTP transport instance
984
+ * @param storageId - Optional storage-source key for multi-backend routing.
985
+ * When set, it is forwarded to the server so the correct
986
+ * `StorageController` is resolved from the registry.
987
+ */
988
+ function createStorage(transport, storageId) {
1108
989
  const urlsCache = /* @__PURE__ */ new Map();
990
+ /** Append ?storageId=... to a path when multi-backend routing is active. */
991
+ const withStorageId = (path) => {
992
+ if (!storageId) return path;
993
+ return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
994
+ };
1109
995
  async function putObject({ file, key, metadata, bucket }) {
1110
996
  const formData = new FormData();
1111
997
  formData.append("file", file);
1112
998
  if (key) formData.append("key", key);
1113
999
  if (bucket) formData.append("bucket", bucket);
1000
+ if (storageId) formData.append("storageId", storageId);
1114
1001
  if (metadata) {
1115
1002
  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));
1116
1003
  }
1117
- return (await transport.request("/storage/upload", {
1004
+ return (await transport.request(withStorageId("/storage/upload"), {
1118
1005
  method: "POST",
1119
1006
  body: formData,
1120
1007
  headers: {}
@@ -1125,18 +1012,18 @@
1125
1012
  const cached = urlsCache.get(cacheKey);
1126
1013
  if (cached) return cached;
1127
1014
  let filePath = keyOrUrl;
1128
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1015
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1129
1016
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1130
1017
  if (!filePath || filePath.trim() === "" || filePath === "/") return {
1131
1018
  url: null,
1132
1019
  fileNotFound: true
1133
1020
  };
1134
1021
  try {
1135
- const result = await transport.request(`/storage/metadata/${filePath}`);
1022
+ const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
1136
1023
  const activeToken = await transport.resolveToken();
1137
1024
  const tokenQuery = activeToken ? `?token=${activeToken}` : "";
1138
1025
  const downloadConfig = {
1139
- url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
1026
+ url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
1140
1027
  metadata: result.data
1141
1028
  };
1142
1029
  urlsCache.set(cacheKey, downloadConfig);
@@ -1151,10 +1038,10 @@
1151
1038
  }
1152
1039
  async function getObject(key, bucket) {
1153
1040
  let filePath = key;
1154
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1041
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1155
1042
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1156
1043
  if (!filePath || filePath.trim() === "" || filePath === "/") return null;
1157
- const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
1044
+ const url = withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`);
1158
1045
  const response = await transport.fetchFn(url, { headers: transport.getHeaders ? transport.getHeaders() : {} });
1159
1046
  if (response.status === 404) return null;
1160
1047
  if (!response.ok) throw new Error("Failed to get file");
@@ -1164,11 +1051,11 @@
1164
1051
  }
1165
1052
  async function deleteObject(key, bucket) {
1166
1053
  let filePath = key;
1167
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1054
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
1168
1055
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
1169
1056
  if (!filePath || filePath.trim() === "" || filePath === "/") return;
1170
1057
  try {
1171
- await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
1058
+ await transport.request(withStorageId(`/storage/file/${filePath}`), { method: "DELETE" });
1172
1059
  } catch (e) {
1173
1060
  if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
1174
1061
  }
@@ -1180,6 +1067,7 @@
1180
1067
  if (options?.bucket) params.set("bucket", options.bucket);
1181
1068
  if (options?.maxResults) params.set("maxResults", String(options.maxResults));
1182
1069
  if (options?.pageToken) params.set("pageToken", options.pageToken);
1070
+ if (storageId) params.set("storageId", storageId);
1183
1071
  return (await transport.request(`/storage/list?${params.toString()}`)).data;
1184
1072
  }
1185
1073
  return {
@@ -1191,6 +1079,62 @@
1191
1079
  };
1192
1080
  }
1193
1081
  //#endregion
1082
+ //#region src/storage-registry.ts
1083
+ /**
1084
+ * Default implementation of the client-side `StorageSourceRegistry`.
1085
+ */
1086
+ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
1087
+ sources = /* @__PURE__ */ new Map();
1088
+ /**
1089
+ * Register a storage source.
1090
+ * @param key - Unique key matching a `StorageSourceDefinition.key`
1091
+ * @param source - The `StorageSource` instance
1092
+ */
1093
+ register(key, source) {
1094
+ this.sources.set(key, source);
1095
+ }
1096
+ getDefault() {
1097
+ const source = this.sources.get(_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY);
1098
+ if (!source) throw new Error(`[StorageSourceRegistry] No default storage source registered. Register one with key "${_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY}".`);
1099
+ return source;
1100
+ }
1101
+ get(key) {
1102
+ if (key === void 0 || key === null) return this.sources.get(_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY);
1103
+ return this.sources.get(key);
1104
+ }
1105
+ getOrDefault(key) {
1106
+ if (key === void 0 || key === null) return this.getDefault();
1107
+ const source = this.sources.get(key);
1108
+ if (source) return source;
1109
+ console.warn(`[StorageSourceRegistry] Storage source "${key}" not found, falling back to "${_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY}".`);
1110
+ return this.getDefault();
1111
+ }
1112
+ has(key) {
1113
+ return this.sources.has(key);
1114
+ }
1115
+ list() {
1116
+ return Array.from(this.sources.keys());
1117
+ }
1118
+ /**
1119
+ * Build a registry from `StorageSourceDefinition[]` and an HTTP transport.
1120
+ *
1121
+ * - Sources with `transport: "server"` are auto-wired via `createStorage(transport, key)`.
1122
+ * - Sources with `transport: "direct"` are **not** auto-wired — they must
1123
+ * be registered manually after this call (e.g. via a Firebase hook).
1124
+ *
1125
+ * @param definitions - Array of storage source definitions
1126
+ * @param transport - HTTP transport for server-backed sources
1127
+ */
1128
+ static fromDefinitions(definitions, transport) {
1129
+ const registry = new ClientStorageSourceRegistry();
1130
+ for (const def of definitions) if (def.transport === "server") {
1131
+ const source = createStorage(transport, def.key === _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY ? void 0 : def.key);
1132
+ registry.register(def.key, source);
1133
+ }
1134
+ return registry;
1135
+ }
1136
+ };
1137
+ //#endregion
1194
1138
  //#region src/websocket.ts
1195
1139
  /**
1196
1140
  * Extract error message and code from a WebSocket message payload.
@@ -2128,6 +2072,23 @@
2128
2072
  const apiKeys = createApiKeys(transport, options.apiKeys);
2129
2073
  const storage = createStorage(transport);
2130
2074
  const functions = createFunctionsClient(transport);
2075
+ const createStorageSource = (storageId) => storageId === _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
2076
+ const storageRegistry = new ClientStorageSourceRegistry();
2077
+ storageRegistry.register(_rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY, storage);
2078
+ for (const def of options.storageSources ?? []) if (def.transport === "server" && def.key !== _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY) storageRegistry.register(def.key, createStorageSource(def.key));
2079
+ let storageSourcesPromise;
2080
+ const fetchStorageSources = () => {
2081
+ if (storageSourcesPromise) return storageSourcesPromise;
2082
+ storageSourcesPromise = transport.request("/storage/sources").then((res) => {
2083
+ const defs = res.data ?? [];
2084
+ for (const def of defs) if (def.transport === "server" && def.key !== _rebasepro_types.DEFAULT_STORAGE_SOURCE_KEY && !storageRegistry.has(def.key)) storageRegistry.register(def.key, createStorageSource(def.key));
2085
+ return defs;
2086
+ }).catch((e) => {
2087
+ storageSourcesPromise = void 0;
2088
+ throw e;
2089
+ });
2090
+ return storageSourcesPromise;
2091
+ };
2131
2092
  const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
2132
2093
  let ws;
2133
2094
  if (resolvedWsUrl) {
@@ -2173,7 +2134,10 @@
2173
2134
  const dataProxy = new Proxy({ collection }, { get(_target, prop) {
2174
2135
  if (prop === "collection") return collection;
2175
2136
  if (typeof prop === "symbol") return void 0;
2176
- if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") return collection((0, _rebasepro_utils.toSnakeCase)(prop));
2137
+ if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
2138
+ if (options.collections && prop in options.collections) return collection(options.collections[prop]);
2139
+ return collection((0, _rebasepro_utils.toSnakeCase)(prop));
2140
+ }
2177
2141
  } });
2178
2142
  return {
2179
2143
  auth,
@@ -2182,6 +2146,9 @@
2182
2146
  apiKeys,
2183
2147
  functions,
2184
2148
  storage,
2149
+ storageRegistry,
2150
+ createStorageSource,
2151
+ fetchStorageSources,
2185
2152
  ws,
2186
2153
  setToken: transport.setToken,
2187
2154
  setAuthTokenGetter: transport.setAuthTokenGetter,
@@ -2203,6 +2170,7 @@
2203
2170
  }
2204
2171
  //#endregion
2205
2172
  exports.ApiError = ApiError;
2173
+ exports.ClientStorageSourceRegistry = ClientStorageSourceRegistry;
2206
2174
  Object.defineProperty(exports, "QueryBuilder", {
2207
2175
  enumerable: true,
2208
2176
  get: function() {