@afribase/afribase-js 0.2.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/README.md +55 -0
- package/dist/index.d.mts +1113 -0
- package/dist/index.d.ts +1113 -0
- package/dist/index.js +1979 -0
- package/dist/index.mjs +1903 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1979 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AfribaseAuthClient: () => AfribaseAuthClient,
|
|
34
|
+
AfribaseClient: () => AfribaseClient,
|
|
35
|
+
AfribaseFunctionsClient: () => AfribaseFunctionsClient,
|
|
36
|
+
AfribaseQueryBuilder: () => AfribaseQueryBuilder,
|
|
37
|
+
AfribaseRealtimeClient: () => AfribaseRealtimeClient,
|
|
38
|
+
AfribaseRpcBuilder: () => AfribaseRpcBuilder,
|
|
39
|
+
AfribaseStorageClient: () => AfribaseStorageClient,
|
|
40
|
+
RealtimeChannel: () => RealtimeChannel,
|
|
41
|
+
StorageFileApi: () => StorageFileApi,
|
|
42
|
+
containedBy: () => containedBy,
|
|
43
|
+
contains: () => contains,
|
|
44
|
+
createClient: () => createClient,
|
|
45
|
+
csv: () => csv,
|
|
46
|
+
db: () => db,
|
|
47
|
+
del: () => del,
|
|
48
|
+
eq: () => eq,
|
|
49
|
+
explain: () => explain,
|
|
50
|
+
geojson: () => geojson,
|
|
51
|
+
gt: () => gt,
|
|
52
|
+
gte: () => gte,
|
|
53
|
+
ilike: () => ilike,
|
|
54
|
+
insert: () => insert,
|
|
55
|
+
is: () => is,
|
|
56
|
+
isIn: () => isIn,
|
|
57
|
+
like: () => like,
|
|
58
|
+
limit: () => limit,
|
|
59
|
+
lt: () => lt,
|
|
60
|
+
lte: () => lte,
|
|
61
|
+
match: () => match,
|
|
62
|
+
maybeSingle: () => maybeSingle,
|
|
63
|
+
neq: () => neq,
|
|
64
|
+
not: () => not,
|
|
65
|
+
or: () => or,
|
|
66
|
+
order: () => order,
|
|
67
|
+
range: () => range,
|
|
68
|
+
select: () => select,
|
|
69
|
+
single: () => single,
|
|
70
|
+
textSearch: () => textSearch,
|
|
71
|
+
update: () => update,
|
|
72
|
+
upsert: () => upsert
|
|
73
|
+
});
|
|
74
|
+
module.exports = __toCommonJS(index_exports);
|
|
75
|
+
|
|
76
|
+
// src/lib/fetch.ts
|
|
77
|
+
var import_cross_fetch = __toESM(require("cross-fetch"));
|
|
78
|
+
var _fetch = typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch : import_cross_fetch.default;
|
|
79
|
+
async function resolveFetch(customFetch) {
|
|
80
|
+
if (customFetch) return customFetch;
|
|
81
|
+
return _fetch;
|
|
82
|
+
}
|
|
83
|
+
async function fetchWithAuth(url, options) {
|
|
84
|
+
const fetchFn = await resolveFetch(options.customFetch);
|
|
85
|
+
const headers = {
|
|
86
|
+
"apikey": options.apiKey,
|
|
87
|
+
...options.headers
|
|
88
|
+
};
|
|
89
|
+
if (options.accessToken) {
|
|
90
|
+
headers["Authorization"] = `Bearer ${options.accessToken}`;
|
|
91
|
+
} else {
|
|
92
|
+
headers["Authorization"] = `Bearer ${options.apiKey}`;
|
|
93
|
+
}
|
|
94
|
+
return fetchFn(url, {
|
|
95
|
+
method: options.method || "GET",
|
|
96
|
+
headers,
|
|
97
|
+
body: options.body
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function handleResponse(response) {
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
let errorBody;
|
|
103
|
+
try {
|
|
104
|
+
errorBody = await response.json();
|
|
105
|
+
} catch {
|
|
106
|
+
errorBody = { message: await response.text() };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
data: null,
|
|
110
|
+
error: {
|
|
111
|
+
message: errorBody.message || errorBody.msg || `Request failed with status ${response.status}`,
|
|
112
|
+
status: response.status,
|
|
113
|
+
...errorBody
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (response.status === 204) {
|
|
118
|
+
return { data: null, error: null };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
return { data, error: null };
|
|
123
|
+
} catch {
|
|
124
|
+
return { data: null, error: null };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/lib/AfribaseAuthClient.ts
|
|
129
|
+
var STORAGE_KEY = "afribase.auth.token";
|
|
130
|
+
var AfribaseAuthClient = class {
|
|
131
|
+
constructor(options) {
|
|
132
|
+
this.currentSession = null;
|
|
133
|
+
this.refreshTimer = null;
|
|
134
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
135
|
+
this.listenerIdCounter = 0;
|
|
136
|
+
this.url = options.url.replace(/\/$/, "");
|
|
137
|
+
this.apiKey = options.apiKey;
|
|
138
|
+
this.autoRefresh = options.autoRefreshToken ?? true;
|
|
139
|
+
this.persistSession = options.persistSession ?? true;
|
|
140
|
+
this.customFetch = options.customFetch;
|
|
141
|
+
}
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
143
|
+
// Session Management
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Initialize the auth client — restores persisted session and emits INITIAL_SESSION.
|
|
147
|
+
* Call this once after creating the client.
|
|
148
|
+
*/
|
|
149
|
+
async initialize() {
|
|
150
|
+
try {
|
|
151
|
+
const stored = this._getStoredSession();
|
|
152
|
+
if (stored) {
|
|
153
|
+
if (this._isSessionExpired(stored)) {
|
|
154
|
+
const { data, error } = await this._refreshSession(stored.refresh_token);
|
|
155
|
+
if (!error && data) {
|
|
156
|
+
this._setSession(data);
|
|
157
|
+
this._notifyListeners("INITIAL_SESSION", data);
|
|
158
|
+
return { data: { session: data }, error: null };
|
|
159
|
+
}
|
|
160
|
+
this._removeSession();
|
|
161
|
+
this._notifyListeners("INITIAL_SESSION", null);
|
|
162
|
+
return { data: { session: null }, error: null };
|
|
163
|
+
}
|
|
164
|
+
this._setSession(stored);
|
|
165
|
+
this._notifyListeners("INITIAL_SESSION", stored);
|
|
166
|
+
return { data: { session: stored }, error: null };
|
|
167
|
+
}
|
|
168
|
+
this._notifyListeners("INITIAL_SESSION", null);
|
|
169
|
+
return { data: { session: null }, error: null };
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return { data: { session: null }, error: err };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Returns the current session if it exists.
|
|
176
|
+
*/
|
|
177
|
+
async getSession() {
|
|
178
|
+
return { data: { session: this.currentSession }, error: null };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Returns the current user if authenticated.
|
|
182
|
+
*/
|
|
183
|
+
async getUser() {
|
|
184
|
+
if (!this.currentSession?.access_token) {
|
|
185
|
+
return { data: { user: null }, error: { message: "Not authenticated" } };
|
|
186
|
+
}
|
|
187
|
+
const resp = await fetchWithAuth(`${this.url}/user`, {
|
|
188
|
+
apiKey: this.apiKey,
|
|
189
|
+
accessToken: this.currentSession.access_token,
|
|
190
|
+
customFetch: this.customFetch
|
|
191
|
+
});
|
|
192
|
+
const result = await handleResponse(resp);
|
|
193
|
+
if (result.data && this.currentSession) {
|
|
194
|
+
this.currentSession.user = result.data;
|
|
195
|
+
}
|
|
196
|
+
return { data: { user: result.data }, error: result.error };
|
|
197
|
+
}
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
199
|
+
// Sign Up / Sign In
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
201
|
+
/**
|
|
202
|
+
* Create a new user with email/phone and password.
|
|
203
|
+
*/
|
|
204
|
+
async signUp(credentials) {
|
|
205
|
+
const body = { password: credentials.password };
|
|
206
|
+
if (credentials.email) body.email = credentials.email;
|
|
207
|
+
if (credentials.phone) body.phone = credentials.phone;
|
|
208
|
+
if (credentials.options?.data) body.data = credentials.options.data;
|
|
209
|
+
if (credentials.options?.captchaToken) body.gotcha = credentials.options.captchaToken;
|
|
210
|
+
const resp = await fetchWithAuth(`${this.url}/signup`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "Content-Type": "application/json" },
|
|
213
|
+
body: JSON.stringify(body),
|
|
214
|
+
apiKey: this.apiKey,
|
|
215
|
+
customFetch: this.customFetch
|
|
216
|
+
});
|
|
217
|
+
const result = await handleResponse(resp);
|
|
218
|
+
if (result.error) return { data: { user: null, session: null }, error: result.error };
|
|
219
|
+
const data = result.data;
|
|
220
|
+
if (data.access_token) {
|
|
221
|
+
const session = this._makeSession(data);
|
|
222
|
+
this._setSession(session);
|
|
223
|
+
this._notifyListeners("SIGNED_IN", session);
|
|
224
|
+
return { data: { user: session.user, session }, error: null };
|
|
225
|
+
}
|
|
226
|
+
return { data: { user: data, session: null }, error: null };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Sign in with email/phone and password.
|
|
230
|
+
*/
|
|
231
|
+
async signInWithPassword(credentials) {
|
|
232
|
+
const body = { password: credentials.password };
|
|
233
|
+
if (credentials.email) body.email = credentials.email;
|
|
234
|
+
if (credentials.phone) body.phone = credentials.phone;
|
|
235
|
+
if (credentials.options?.captchaToken) body.gotcha = credentials.options.captchaToken;
|
|
236
|
+
const resp = await fetchWithAuth(`${this.url}/token?grant_type=password`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify(body),
|
|
240
|
+
apiKey: this.apiKey,
|
|
241
|
+
customFetch: this.customFetch
|
|
242
|
+
});
|
|
243
|
+
const result = await handleResponse(resp);
|
|
244
|
+
if (result.error) return { data: { user: null, session: null }, error: result.error };
|
|
245
|
+
const session = this._makeSession(result.data);
|
|
246
|
+
this._setSession(session);
|
|
247
|
+
this._notifyListeners("SIGNED_IN", session);
|
|
248
|
+
return { data: { user: session.user, session }, error: null };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Sign in with a third-party OAuth provider.
|
|
252
|
+
* Returns a URL to redirect the user to.
|
|
253
|
+
*/
|
|
254
|
+
async signInWithOAuth(credentials) {
|
|
255
|
+
const params = new URLSearchParams();
|
|
256
|
+
if (credentials.options?.redirectTo) params.set("redirect_to", credentials.options.redirectTo);
|
|
257
|
+
if (credentials.options?.scopes) params.set("scopes", credentials.options.scopes);
|
|
258
|
+
if (credentials.options?.queryParams) {
|
|
259
|
+
for (const [k, v] of Object.entries(credentials.options.queryParams)) {
|
|
260
|
+
params.set(k, v);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const qs = params.toString();
|
|
264
|
+
const url = `${this.url}/authorize?provider=${credentials.provider}${qs ? "&" + qs : ""}`;
|
|
265
|
+
return { data: { provider: credentials.provider, url }, error: null };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Sign in with a one-time password (magic link or SMS OTP).
|
|
269
|
+
*/
|
|
270
|
+
async signInWithOtp(credentials) {
|
|
271
|
+
const body = {};
|
|
272
|
+
if (credentials.email) body.email = credentials.email;
|
|
273
|
+
if (credentials.phone) body.phone = credentials.phone;
|
|
274
|
+
if (credentials.options?.data) body.data = credentials.options.data;
|
|
275
|
+
if (credentials.options?.captchaToken) body.gotcha = credentials.options.captchaToken;
|
|
276
|
+
if (credentials.options?.shouldCreateUser !== void 0) body.create_user = credentials.options.shouldCreateUser;
|
|
277
|
+
const resp = await fetchWithAuth(`${this.url}/otp`, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: { "Content-Type": "application/json" },
|
|
280
|
+
body: JSON.stringify(body),
|
|
281
|
+
apiKey: this.apiKey,
|
|
282
|
+
customFetch: this.customFetch
|
|
283
|
+
});
|
|
284
|
+
return handleResponse(resp);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Verify an OTP or magic link token.
|
|
288
|
+
*/
|
|
289
|
+
async verifyOtp(params) {
|
|
290
|
+
const body = { token: params.token, type: params.type };
|
|
291
|
+
if (params.email) body.email = params.email;
|
|
292
|
+
if (params.phone) body.phone = params.phone;
|
|
293
|
+
const resp = await fetchWithAuth(`${this.url}/verify`, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { "Content-Type": "application/json" },
|
|
296
|
+
body: JSON.stringify(body),
|
|
297
|
+
apiKey: this.apiKey,
|
|
298
|
+
customFetch: this.customFetch
|
|
299
|
+
});
|
|
300
|
+
const result = await handleResponse(resp);
|
|
301
|
+
if (result.error) return { data: { user: null, session: null }, error: result.error };
|
|
302
|
+
if (result.data?.access_token) {
|
|
303
|
+
const session = this._makeSession(result.data);
|
|
304
|
+
this._setSession(session);
|
|
305
|
+
this._notifyListeners("SIGNED_IN", session);
|
|
306
|
+
return { data: { user: session.user, session }, error: null };
|
|
307
|
+
}
|
|
308
|
+
return { data: result.data, error: null };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Sign in anonymously — creates an anonymous user.
|
|
312
|
+
*/
|
|
313
|
+
async signInAnonymously(options) {
|
|
314
|
+
const body = {};
|
|
315
|
+
if (options?.data) body.data = options.data;
|
|
316
|
+
if (options?.captchaToken) body.gotcha = options.captchaToken;
|
|
317
|
+
const resp = await fetchWithAuth(`${this.url}/signup`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { "Content-Type": "application/json" },
|
|
320
|
+
body: JSON.stringify(body),
|
|
321
|
+
apiKey: this.apiKey,
|
|
322
|
+
customFetch: this.customFetch
|
|
323
|
+
});
|
|
324
|
+
const result = await handleResponse(resp);
|
|
325
|
+
if (result.error) return { data: { user: null, session: null }, error: result.error };
|
|
326
|
+
if (result.data?.access_token) {
|
|
327
|
+
const session = this._makeSession(result.data);
|
|
328
|
+
this._setSession(session);
|
|
329
|
+
this._notifyListeners("SIGNED_IN", session);
|
|
330
|
+
return { data: { user: session.user, session }, error: null };
|
|
331
|
+
}
|
|
332
|
+
return { data: result.data, error: null };
|
|
333
|
+
}
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
335
|
+
// Password Recovery & User Updates
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
337
|
+
/**
|
|
338
|
+
* Sends a password reset email.
|
|
339
|
+
*/
|
|
340
|
+
async resetPasswordForEmail(email, options) {
|
|
341
|
+
const body = { email };
|
|
342
|
+
if (options?.redirectTo) body.redirect_to = options.redirectTo;
|
|
343
|
+
if (options?.captchaToken) body.gotcha = options.captchaToken;
|
|
344
|
+
const resp = await fetchWithAuth(`${this.url}/recover`, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: { "Content-Type": "application/json" },
|
|
347
|
+
body: JSON.stringify(body),
|
|
348
|
+
apiKey: this.apiKey,
|
|
349
|
+
customFetch: this.customFetch
|
|
350
|
+
});
|
|
351
|
+
return handleResponse(resp);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Update the current user (email, phone, password, metadata).
|
|
355
|
+
*/
|
|
356
|
+
async updateUser(attributes) {
|
|
357
|
+
if (!this.currentSession?.access_token) {
|
|
358
|
+
return { data: { user: null }, error: { message: "Not authenticated" } };
|
|
359
|
+
}
|
|
360
|
+
const resp = await fetchWithAuth(`${this.url}/user`, {
|
|
361
|
+
method: "PUT",
|
|
362
|
+
headers: { "Content-Type": "application/json" },
|
|
363
|
+
body: JSON.stringify(attributes),
|
|
364
|
+
apiKey: this.apiKey,
|
|
365
|
+
accessToken: this.currentSession.access_token,
|
|
366
|
+
customFetch: this.customFetch
|
|
367
|
+
});
|
|
368
|
+
const result = await handleResponse(resp);
|
|
369
|
+
if (!result.error && result.data && this.currentSession) {
|
|
370
|
+
this.currentSession.user = result.data;
|
|
371
|
+
this._persistSession(this.currentSession);
|
|
372
|
+
this._notifyListeners("USER_UPDATED", this.currentSession);
|
|
373
|
+
}
|
|
374
|
+
return { data: { user: result.data }, error: result.error };
|
|
375
|
+
}
|
|
376
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
377
|
+
// Sign Out
|
|
378
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
379
|
+
/**
|
|
380
|
+
* Sign out the current user.
|
|
381
|
+
*/
|
|
382
|
+
async signOut() {
|
|
383
|
+
if (this.currentSession?.access_token) {
|
|
384
|
+
try {
|
|
385
|
+
await fetchWithAuth(`${this.url}/logout`, {
|
|
386
|
+
method: "POST",
|
|
387
|
+
apiKey: this.apiKey,
|
|
388
|
+
accessToken: this.currentSession.access_token,
|
|
389
|
+
customFetch: this.customFetch
|
|
390
|
+
});
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
this._removeSession();
|
|
395
|
+
this._notifyListeners("SIGNED_OUT", null);
|
|
396
|
+
return { error: null };
|
|
397
|
+
}
|
|
398
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
399
|
+
// Auth State Change
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
401
|
+
/**
|
|
402
|
+
* Listen for auth state changes.
|
|
403
|
+
* Returns a subscription object with an unsubscribe method.
|
|
404
|
+
*/
|
|
405
|
+
onAuthStateChange(callback) {
|
|
406
|
+
const id = `auth-listener-${++this.listenerIdCounter}`;
|
|
407
|
+
this.listeners.set(id, callback);
|
|
408
|
+
setTimeout(() => {
|
|
409
|
+
callback("INITIAL_SESSION", this.currentSession);
|
|
410
|
+
}, 0);
|
|
411
|
+
return {
|
|
412
|
+
data: {
|
|
413
|
+
subscription: {
|
|
414
|
+
id,
|
|
415
|
+
unsubscribe: () => {
|
|
416
|
+
this.listeners.delete(id);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
423
|
+
// Token Refresh
|
|
424
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
425
|
+
/**
|
|
426
|
+
* Manually refresh the session token.
|
|
427
|
+
*/
|
|
428
|
+
async refreshSession() {
|
|
429
|
+
if (!this.currentSession?.refresh_token) {
|
|
430
|
+
return { data: { session: null }, error: { message: "No refresh token" } };
|
|
431
|
+
}
|
|
432
|
+
const { data, error } = await this._refreshSession(this.currentSession.refresh_token);
|
|
433
|
+
if (error) return { data: { session: null }, error };
|
|
434
|
+
if (data) {
|
|
435
|
+
this._setSession(data);
|
|
436
|
+
this._notifyListeners("TOKEN_REFRESHED", data);
|
|
437
|
+
}
|
|
438
|
+
return { data: { session: data }, error: null };
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Set the session manually (useful for server-side auth).
|
|
442
|
+
*/
|
|
443
|
+
async setSession(params) {
|
|
444
|
+
const resp = await fetchWithAuth(`${this.url}/user`, {
|
|
445
|
+
apiKey: this.apiKey,
|
|
446
|
+
accessToken: params.access_token,
|
|
447
|
+
customFetch: this.customFetch
|
|
448
|
+
});
|
|
449
|
+
const result = await handleResponse(resp);
|
|
450
|
+
if (result.error) return { data: { session: null }, error: result.error };
|
|
451
|
+
const session = {
|
|
452
|
+
access_token: params.access_token,
|
|
453
|
+
refresh_token: params.refresh_token,
|
|
454
|
+
expires_in: 3600,
|
|
455
|
+
expires_at: Math.floor(Date.now() / 1e3) + 3600,
|
|
456
|
+
token_type: "bearer",
|
|
457
|
+
user: result.data
|
|
458
|
+
};
|
|
459
|
+
this._setSession(session);
|
|
460
|
+
this._notifyListeners("SIGNED_IN", session);
|
|
461
|
+
return { data: { session }, error: null };
|
|
462
|
+
}
|
|
463
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
464
|
+
// Helpers — Access Token for other clients
|
|
465
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
466
|
+
/** @internal */
|
|
467
|
+
get accessToken() {
|
|
468
|
+
return this.currentSession?.access_token ?? null;
|
|
469
|
+
}
|
|
470
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
471
|
+
// Private Helpers
|
|
472
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
473
|
+
_makeSession(data) {
|
|
474
|
+
const expiresAt = data.expires_at ?? Math.floor(Date.now() / 1e3) + (data.expires_in || 3600);
|
|
475
|
+
return {
|
|
476
|
+
access_token: data.access_token,
|
|
477
|
+
refresh_token: data.refresh_token,
|
|
478
|
+
expires_in: data.expires_in || 3600,
|
|
479
|
+
expires_at: expiresAt,
|
|
480
|
+
token_type: data.token_type || "bearer",
|
|
481
|
+
user: data.user || data
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
_setSession(session) {
|
|
485
|
+
this.currentSession = session;
|
|
486
|
+
this._persistSession(session);
|
|
487
|
+
this._scheduleRefresh(session);
|
|
488
|
+
}
|
|
489
|
+
_removeSession() {
|
|
490
|
+
this.currentSession = null;
|
|
491
|
+
if (this.refreshTimer) {
|
|
492
|
+
clearTimeout(this.refreshTimer);
|
|
493
|
+
this.refreshTimer = null;
|
|
494
|
+
}
|
|
495
|
+
this._clearStoredSession();
|
|
496
|
+
}
|
|
497
|
+
async _refreshSession(refreshToken) {
|
|
498
|
+
const resp = await fetchWithAuth(`${this.url}/token?grant_type=refresh_token`, {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: { "Content-Type": "application/json" },
|
|
501
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
502
|
+
apiKey: this.apiKey,
|
|
503
|
+
customFetch: this.customFetch
|
|
504
|
+
});
|
|
505
|
+
const result = await handleResponse(resp);
|
|
506
|
+
if (result.error) return { data: null, error: result.error };
|
|
507
|
+
return { data: this._makeSession(result.data), error: null };
|
|
508
|
+
}
|
|
509
|
+
_scheduleRefresh(session) {
|
|
510
|
+
if (!this.autoRefresh) return;
|
|
511
|
+
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
512
|
+
const expiresAt = session.expires_at ?? Math.floor(Date.now() / 1e3) + session.expires_in;
|
|
513
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
514
|
+
const refreshIn = Math.max((expiresAt - now - 60) * 1e3, 1e3);
|
|
515
|
+
this.refreshTimer = setTimeout(async () => {
|
|
516
|
+
if (this.currentSession?.refresh_token) {
|
|
517
|
+
const { data, error } = await this._refreshSession(this.currentSession.refresh_token);
|
|
518
|
+
if (!error && data) {
|
|
519
|
+
this._setSession(data);
|
|
520
|
+
this._notifyListeners("TOKEN_REFRESHED", data);
|
|
521
|
+
} else {
|
|
522
|
+
this._removeSession();
|
|
523
|
+
this._notifyListeners("SIGNED_OUT", null);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}, refreshIn);
|
|
527
|
+
}
|
|
528
|
+
_isSessionExpired(session) {
|
|
529
|
+
if (!session.expires_at) return false;
|
|
530
|
+
return Math.floor(Date.now() / 1e3) >= session.expires_at;
|
|
531
|
+
}
|
|
532
|
+
_notifyListeners(event, session) {
|
|
533
|
+
for (const cb of this.listeners.values()) {
|
|
534
|
+
try {
|
|
535
|
+
cb(event, session);
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Storage helpers (localStorage in browser, no-op in Node)
|
|
541
|
+
_persistSession(session) {
|
|
542
|
+
if (!this.persistSession) return;
|
|
543
|
+
try {
|
|
544
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
545
|
+
globalThis.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
546
|
+
}
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
_getStoredSession() {
|
|
551
|
+
if (!this.persistSession) return null;
|
|
552
|
+
try {
|
|
553
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
554
|
+
const raw = globalThis.localStorage.getItem(STORAGE_KEY);
|
|
555
|
+
return raw ? JSON.parse(raw) : null;
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
_clearStoredSession() {
|
|
562
|
+
try {
|
|
563
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
564
|
+
globalThis.localStorage.removeItem(STORAGE_KEY);
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// src/lib/AfribaseRealtimeClient.ts
|
|
572
|
+
function getWebSocketImpl() {
|
|
573
|
+
if (typeof WebSocket !== "undefined") return WebSocket;
|
|
574
|
+
try {
|
|
575
|
+
const ws = globalThis.require?.("ws") ?? new Function('return require("ws")')();
|
|
576
|
+
return ws;
|
|
577
|
+
} catch {
|
|
578
|
+
throw new Error(
|
|
579
|
+
'WebSocket is not available. In Node.js, install the "ws" package: npm install ws'
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
var WS_OPEN = 1;
|
|
584
|
+
var RealtimeChannel = class {
|
|
585
|
+
constructor(topic, params, sendMessage, removeChannel) {
|
|
586
|
+
this.params = params;
|
|
587
|
+
this.sendMessage = sendMessage;
|
|
588
|
+
this.removeChannel = removeChannel;
|
|
589
|
+
this.bindings = [];
|
|
590
|
+
this.presenceState = {};
|
|
591
|
+
this._status = "CLOSED";
|
|
592
|
+
this.topic = topic;
|
|
593
|
+
this.presenceKey = params.config?.presence?.key || "";
|
|
594
|
+
}
|
|
595
|
+
on(type, filter, callback) {
|
|
596
|
+
this.bindings.push({ type, filter, callback });
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Subscribe to the channel — starts receiving events.
|
|
601
|
+
*/
|
|
602
|
+
subscribe(callback) {
|
|
603
|
+
const joinConfig = { topic: this.topic };
|
|
604
|
+
const pgBindings = this.bindings.filter((b) => b.type === "postgres_changes");
|
|
605
|
+
if (pgBindings.length > 0) {
|
|
606
|
+
joinConfig.postgres_changes = pgBindings.map((b) => ({
|
|
607
|
+
event: b.filter?.event || "*",
|
|
608
|
+
schema: b.filter?.schema || "public",
|
|
609
|
+
table: b.filter?.table || "*",
|
|
610
|
+
filter: b.filter?.filter
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
if (this.params.config?.broadcast) {
|
|
614
|
+
joinConfig.broadcast = this.params.config.broadcast;
|
|
615
|
+
}
|
|
616
|
+
if (this.params.config?.presence) {
|
|
617
|
+
joinConfig.presence = this.params.config.presence;
|
|
618
|
+
}
|
|
619
|
+
this.sendMessage({
|
|
620
|
+
topic: this.topic,
|
|
621
|
+
event: "phx_join",
|
|
622
|
+
payload: joinConfig,
|
|
623
|
+
ref: null
|
|
624
|
+
});
|
|
625
|
+
this._status = "SUBSCRIBED";
|
|
626
|
+
if (callback) {
|
|
627
|
+
setTimeout(() => callback("SUBSCRIBED"), 0);
|
|
628
|
+
}
|
|
629
|
+
return this;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Send a broadcast message to all subscribers.
|
|
633
|
+
*/
|
|
634
|
+
send(payload) {
|
|
635
|
+
this.sendMessage({
|
|
636
|
+
topic: this.topic,
|
|
637
|
+
event: payload.event,
|
|
638
|
+
payload: payload.payload,
|
|
639
|
+
ref: null
|
|
640
|
+
});
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Track presence for the current user.
|
|
645
|
+
*/
|
|
646
|
+
track(payload) {
|
|
647
|
+
this.sendMessage({
|
|
648
|
+
topic: this.topic,
|
|
649
|
+
event: "presence",
|
|
650
|
+
payload: { type: "track", ...payload, key: this.presenceKey },
|
|
651
|
+
ref: null
|
|
652
|
+
});
|
|
653
|
+
return this;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Untrack presence for the current user.
|
|
657
|
+
*/
|
|
658
|
+
untrack() {
|
|
659
|
+
this.sendMessage({
|
|
660
|
+
topic: this.topic,
|
|
661
|
+
event: "presence",
|
|
662
|
+
payload: { type: "untrack", key: this.presenceKey },
|
|
663
|
+
ref: null
|
|
664
|
+
});
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Get current presence state.
|
|
669
|
+
*/
|
|
670
|
+
presenceState_() {
|
|
671
|
+
return { ...this.presenceState };
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Unsubscribe from this channel.
|
|
675
|
+
*/
|
|
676
|
+
unsubscribe() {
|
|
677
|
+
this.sendMessage({
|
|
678
|
+
topic: this.topic,
|
|
679
|
+
event: "phx_leave",
|
|
680
|
+
payload: {},
|
|
681
|
+
ref: null
|
|
682
|
+
});
|
|
683
|
+
this._status = "CLOSED";
|
|
684
|
+
this.removeChannel(this.topic);
|
|
685
|
+
}
|
|
686
|
+
get status() {
|
|
687
|
+
return this._status;
|
|
688
|
+
}
|
|
689
|
+
/** @internal — called by the RealtimeClient when a message arrives */
|
|
690
|
+
_handleMessage(msg) {
|
|
691
|
+
if (msg.payload?.type === "postgres_changes") {
|
|
692
|
+
const pgPayload = msg.payload;
|
|
693
|
+
for (const binding of this.bindings) {
|
|
694
|
+
if (binding.type !== "postgres_changes") continue;
|
|
695
|
+
const f = binding.filter;
|
|
696
|
+
if (f?.schema && f.schema !== pgPayload.schema) continue;
|
|
697
|
+
if (f?.table && f.table !== pgPayload.table) continue;
|
|
698
|
+
if (f?.event && f.event !== "*" && f.event !== pgPayload.eventType) continue;
|
|
699
|
+
binding.callback(pgPayload);
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (msg.payload?.type === "presence_state" || msg.payload?.type === "presence_diff") {
|
|
704
|
+
if (msg.payload.type === "presence_state") {
|
|
705
|
+
this.presenceState = msg.payload.state || {};
|
|
706
|
+
} else if (msg.payload.type === "presence_diff") {
|
|
707
|
+
const { joins, leaves } = msg.payload;
|
|
708
|
+
if (joins) {
|
|
709
|
+
for (const [key, val] of Object.entries(joins)) {
|
|
710
|
+
this.presenceState[key] = val.metas;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (leaves) {
|
|
714
|
+
for (const key of Object.keys(leaves)) {
|
|
715
|
+
delete this.presenceState[key];
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const binding of this.bindings) {
|
|
720
|
+
if (binding.type !== "presence") continue;
|
|
721
|
+
if (binding.filter?.event === "sync" || binding.filter?.event === msg.payload.type) {
|
|
722
|
+
binding.callback({
|
|
723
|
+
type: msg.payload.type,
|
|
724
|
+
presenceState: this.presenceState,
|
|
725
|
+
joins: msg.payload.joins,
|
|
726
|
+
leaves: msg.payload.leaves
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
for (const binding of this.bindings) {
|
|
733
|
+
if (binding.type !== "broadcast") continue;
|
|
734
|
+
if (binding.filter?.event === msg.event || binding.filter?.event === "*") {
|
|
735
|
+
binding.callback({ type: "broadcast", event: msg.event, payload: msg.payload });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
var AfribaseRealtimeClient = class {
|
|
741
|
+
constructor(options) {
|
|
742
|
+
this.ws = null;
|
|
743
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
744
|
+
this.heartbeatTimer = null;
|
|
745
|
+
this.reconnectTimer = null;
|
|
746
|
+
this.reconnectAttempts = 0;
|
|
747
|
+
this.maxReconnectAttempts = 10;
|
|
748
|
+
this.reconnectBaseDelay = 1e3;
|
|
749
|
+
this.connected = false;
|
|
750
|
+
this.url = options.url.replace(/^http/, "ws").replace(/\/$/, "") + "/realtime/v1/websocket";
|
|
751
|
+
this.apiKey = options.apiKey;
|
|
752
|
+
this.accessToken = options.accessToken ?? null;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Set the access token (called by AfribaseClient when auth state changes).
|
|
756
|
+
*/
|
|
757
|
+
setAuth(token) {
|
|
758
|
+
this.accessToken = token;
|
|
759
|
+
if (this.ws && this.ws.readyState === WS_OPEN) {
|
|
760
|
+
this._send({
|
|
761
|
+
topic: "phoenix",
|
|
762
|
+
event: "access_token",
|
|
763
|
+
payload: { access_token: token },
|
|
764
|
+
ref: null
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Create a channel for a given topic.
|
|
770
|
+
*/
|
|
771
|
+
channel(topic, options = {}) {
|
|
772
|
+
const normalizedTopic = topic.startsWith("realtime:") ? topic : `realtime:${topic}`;
|
|
773
|
+
if (this.channels.has(normalizedTopic)) {
|
|
774
|
+
return this.channels.get(normalizedTopic);
|
|
775
|
+
}
|
|
776
|
+
const ch = new RealtimeChannel(
|
|
777
|
+
normalizedTopic,
|
|
778
|
+
options,
|
|
779
|
+
(msg) => this._send(msg),
|
|
780
|
+
(t) => this.channels.delete(t)
|
|
781
|
+
);
|
|
782
|
+
this.channels.set(normalizedTopic, ch);
|
|
783
|
+
if (!this.ws) {
|
|
784
|
+
this._connect();
|
|
785
|
+
}
|
|
786
|
+
return ch;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Remove all channels and disconnect.
|
|
790
|
+
*/
|
|
791
|
+
removeAllChannels() {
|
|
792
|
+
for (const ch of this.channels.values()) {
|
|
793
|
+
ch.unsubscribe();
|
|
794
|
+
}
|
|
795
|
+
this.channels.clear();
|
|
796
|
+
this._disconnect();
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Disconnect the WebSocket.
|
|
800
|
+
*/
|
|
801
|
+
disconnect() {
|
|
802
|
+
this._disconnect();
|
|
803
|
+
}
|
|
804
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
805
|
+
// Private
|
|
806
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
807
|
+
_connect() {
|
|
808
|
+
if (this.ws) return;
|
|
809
|
+
try {
|
|
810
|
+
const WSImpl = getWebSocketImpl();
|
|
811
|
+
const params = new URLSearchParams({
|
|
812
|
+
apikey: this.apiKey,
|
|
813
|
+
vsn: "1.0.0"
|
|
814
|
+
});
|
|
815
|
+
if (this.accessToken) {
|
|
816
|
+
params.set("token", this.accessToken);
|
|
817
|
+
}
|
|
818
|
+
this.ws = new WSImpl(`${this.url}?${params.toString()}`);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
this._scheduleReconnect();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
this.ws.onopen = () => {
|
|
824
|
+
this.connected = true;
|
|
825
|
+
this.reconnectAttempts = 0;
|
|
826
|
+
this._startHeartbeat();
|
|
827
|
+
for (const ch of this.channels.values()) {
|
|
828
|
+
if (ch.status === "CLOSED") {
|
|
829
|
+
ch.subscribe();
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
this.ws.onmessage = (ev) => {
|
|
834
|
+
try {
|
|
835
|
+
const msg = JSON.parse(typeof ev.data === "string" ? ev.data : ev.data.toString());
|
|
836
|
+
this._handleMessage(msg);
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
this.ws.onclose = () => {
|
|
841
|
+
this.connected = false;
|
|
842
|
+
this._stopHeartbeat();
|
|
843
|
+
this.ws = null;
|
|
844
|
+
if (this.channels.size > 0) {
|
|
845
|
+
this._scheduleReconnect();
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
this.ws.onerror = () => {
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
_disconnect() {
|
|
852
|
+
if (this.reconnectTimer) {
|
|
853
|
+
clearTimeout(this.reconnectTimer);
|
|
854
|
+
this.reconnectTimer = null;
|
|
855
|
+
}
|
|
856
|
+
this._stopHeartbeat();
|
|
857
|
+
if (this.ws) {
|
|
858
|
+
this.ws.onclose = null;
|
|
859
|
+
this.ws.close();
|
|
860
|
+
this.ws = null;
|
|
861
|
+
}
|
|
862
|
+
this.connected = false;
|
|
863
|
+
}
|
|
864
|
+
_send(msg) {
|
|
865
|
+
if (this.ws && this.ws.readyState === WS_OPEN) {
|
|
866
|
+
this.ws.send(JSON.stringify(msg));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
_handleMessage(msg) {
|
|
870
|
+
if (msg.topic === "phoenix" && msg.event === "phx_reply") return;
|
|
871
|
+
const ch = this.channels.get(msg.topic);
|
|
872
|
+
if (ch) {
|
|
873
|
+
ch._handleMessage(msg);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
_startHeartbeat() {
|
|
877
|
+
this._stopHeartbeat();
|
|
878
|
+
this.heartbeatTimer = setInterval(() => {
|
|
879
|
+
this._send({ topic: "phoenix", event: "heartbeat", payload: {}, ref: null });
|
|
880
|
+
}, 3e4);
|
|
881
|
+
}
|
|
882
|
+
_stopHeartbeat() {
|
|
883
|
+
if (this.heartbeatTimer) {
|
|
884
|
+
clearInterval(this.heartbeatTimer);
|
|
885
|
+
this.heartbeatTimer = null;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
_scheduleReconnect() {
|
|
889
|
+
if (this.reconnectTimer) return;
|
|
890
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
891
|
+
const delay = Math.min(
|
|
892
|
+
this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts),
|
|
893
|
+
3e4
|
|
894
|
+
);
|
|
895
|
+
this.reconnectAttempts++;
|
|
896
|
+
this.reconnectTimer = setTimeout(() => {
|
|
897
|
+
this.reconnectTimer = null;
|
|
898
|
+
this._connect();
|
|
899
|
+
}, delay);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// src/lib/AfribaseStorageClient.ts
|
|
904
|
+
var AfribaseStorageClient = class {
|
|
905
|
+
constructor(options) {
|
|
906
|
+
this.url = options.url.replace(/\/$/, "");
|
|
907
|
+
this.apiKey = options.apiKey;
|
|
908
|
+
this.getAccessToken = options.getAccessToken;
|
|
909
|
+
this.customFetch = options.customFetch;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get a reference to a specific bucket for file operations.
|
|
913
|
+
*/
|
|
914
|
+
from(bucketId) {
|
|
915
|
+
return new StorageFileApi(
|
|
916
|
+
this.url,
|
|
917
|
+
bucketId,
|
|
918
|
+
this.apiKey,
|
|
919
|
+
this.getAccessToken,
|
|
920
|
+
this.customFetch
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
924
|
+
// Bucket Management
|
|
925
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
926
|
+
/**
|
|
927
|
+
* List all buckets.
|
|
928
|
+
*/
|
|
929
|
+
async listBuckets() {
|
|
930
|
+
const resp = await fetchWithAuth(`${this.url}/bucket`, {
|
|
931
|
+
apiKey: this.apiKey,
|
|
932
|
+
accessToken: this.getAccessToken(),
|
|
933
|
+
customFetch: this.customFetch
|
|
934
|
+
});
|
|
935
|
+
return handleResponse(resp);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Get a bucket by ID.
|
|
939
|
+
*/
|
|
940
|
+
async getBucket(id) {
|
|
941
|
+
const resp = await fetchWithAuth(`${this.url}/bucket/${id}`, {
|
|
942
|
+
apiKey: this.apiKey,
|
|
943
|
+
accessToken: this.getAccessToken(),
|
|
944
|
+
customFetch: this.customFetch
|
|
945
|
+
});
|
|
946
|
+
return handleResponse(resp);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Create a new bucket.
|
|
950
|
+
*/
|
|
951
|
+
async createBucket(id, options) {
|
|
952
|
+
const resp = await fetchWithAuth(`${this.url}/bucket`, {
|
|
953
|
+
method: "POST",
|
|
954
|
+
headers: { "Content-Type": "application/json" },
|
|
955
|
+
body: JSON.stringify({
|
|
956
|
+
id,
|
|
957
|
+
name: id,
|
|
958
|
+
public: options?.public ?? false,
|
|
959
|
+
file_size_limit: options?.fileSizeLimit,
|
|
960
|
+
allowed_mime_types: options?.allowedMimeTypes
|
|
961
|
+
}),
|
|
962
|
+
apiKey: this.apiKey,
|
|
963
|
+
accessToken: this.getAccessToken(),
|
|
964
|
+
customFetch: this.customFetch
|
|
965
|
+
});
|
|
966
|
+
return handleResponse(resp);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Update a bucket.
|
|
970
|
+
*/
|
|
971
|
+
async updateBucket(id, options) {
|
|
972
|
+
const resp = await fetchWithAuth(`${this.url}/bucket/${id}`, {
|
|
973
|
+
method: "PUT",
|
|
974
|
+
headers: { "Content-Type": "application/json" },
|
|
975
|
+
body: JSON.stringify({
|
|
976
|
+
public: options.public,
|
|
977
|
+
file_size_limit: options.fileSizeLimit,
|
|
978
|
+
allowed_mime_types: options.allowedMimeTypes
|
|
979
|
+
}),
|
|
980
|
+
apiKey: this.apiKey,
|
|
981
|
+
accessToken: this.getAccessToken(),
|
|
982
|
+
customFetch: this.customFetch
|
|
983
|
+
});
|
|
984
|
+
return handleResponse(resp);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Delete a bucket (must be empty).
|
|
988
|
+
*/
|
|
989
|
+
async deleteBucket(id) {
|
|
990
|
+
const resp = await fetchWithAuth(`${this.url}/bucket/${id}`, {
|
|
991
|
+
method: "DELETE",
|
|
992
|
+
apiKey: this.apiKey,
|
|
993
|
+
accessToken: this.getAccessToken(),
|
|
994
|
+
customFetch: this.customFetch
|
|
995
|
+
});
|
|
996
|
+
return handleResponse(resp);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Empty a bucket (remove all files).
|
|
1000
|
+
*/
|
|
1001
|
+
async emptyBucket(id) {
|
|
1002
|
+
const resp = await fetchWithAuth(`${this.url}/bucket/${id}/empty`, {
|
|
1003
|
+
method: "POST",
|
|
1004
|
+
apiKey: this.apiKey,
|
|
1005
|
+
accessToken: this.getAccessToken(),
|
|
1006
|
+
customFetch: this.customFetch
|
|
1007
|
+
});
|
|
1008
|
+
return handleResponse(resp);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
var StorageFileApi = class {
|
|
1012
|
+
constructor(storageUrl, bucketId, apiKey, getAccessToken, customFetch) {
|
|
1013
|
+
this.storageUrl = storageUrl;
|
|
1014
|
+
this.bucketId = bucketId;
|
|
1015
|
+
this.apiKey = apiKey;
|
|
1016
|
+
this.getAccessToken = getAccessToken;
|
|
1017
|
+
this.customFetch = customFetch;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Upload a file to the bucket.
|
|
1021
|
+
*/
|
|
1022
|
+
async upload(path, fileBody, options) {
|
|
1023
|
+
const contentType = options?.contentType || "application/octet-stream";
|
|
1024
|
+
const headers = {
|
|
1025
|
+
"Content-Type": contentType
|
|
1026
|
+
};
|
|
1027
|
+
if (options?.cacheControl) headers["Cache-Control"] = options.cacheControl;
|
|
1028
|
+
if (options?.upsert) headers["x-upsert"] = "true";
|
|
1029
|
+
const resp = await fetchWithAuth(
|
|
1030
|
+
`${this.storageUrl}/object/${this.bucketId}/${path}`,
|
|
1031
|
+
{
|
|
1032
|
+
method: "POST",
|
|
1033
|
+
headers,
|
|
1034
|
+
body: fileBody,
|
|
1035
|
+
apiKey: this.apiKey,
|
|
1036
|
+
accessToken: this.getAccessToken(),
|
|
1037
|
+
customFetch: this.customFetch
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
const result = await handleResponse(resp);
|
|
1041
|
+
if (result.error) return { data: null, error: result.error };
|
|
1042
|
+
return {
|
|
1043
|
+
data: {
|
|
1044
|
+
path,
|
|
1045
|
+
id: result.data?.id ?? result.data?.Key,
|
|
1046
|
+
fullPath: `${this.bucketId}/${path}`
|
|
1047
|
+
},
|
|
1048
|
+
error: null
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Update (replace) an existing file.
|
|
1053
|
+
*/
|
|
1054
|
+
async update(path, fileBody, options) {
|
|
1055
|
+
const contentType = options?.contentType || "application/octet-stream";
|
|
1056
|
+
const headers = {
|
|
1057
|
+
"Content-Type": contentType,
|
|
1058
|
+
"x-upsert": "true"
|
|
1059
|
+
};
|
|
1060
|
+
if (options?.cacheControl) headers["Cache-Control"] = options.cacheControl;
|
|
1061
|
+
const resp = await fetchWithAuth(
|
|
1062
|
+
`${this.storageUrl}/object/${this.bucketId}/${path}`,
|
|
1063
|
+
{
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
headers,
|
|
1066
|
+
body: fileBody,
|
|
1067
|
+
apiKey: this.apiKey,
|
|
1068
|
+
accessToken: this.getAccessToken(),
|
|
1069
|
+
customFetch: this.customFetch
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
const result = await handleResponse(resp);
|
|
1073
|
+
if (result.error) return { data: null, error: result.error };
|
|
1074
|
+
return {
|
|
1075
|
+
data: {
|
|
1076
|
+
path,
|
|
1077
|
+
id: result.data?.id ?? result.data?.Key,
|
|
1078
|
+
fullPath: `${this.bucketId}/${path}`
|
|
1079
|
+
},
|
|
1080
|
+
error: null
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Download a file from the bucket.
|
|
1085
|
+
*/
|
|
1086
|
+
async download(path, options) {
|
|
1087
|
+
const params = new URLSearchParams();
|
|
1088
|
+
if (options?.transform) {
|
|
1089
|
+
if (options.transform.width) params.set("width", String(options.transform.width));
|
|
1090
|
+
if (options.transform.height) params.set("height", String(options.transform.height));
|
|
1091
|
+
if (options.transform.resize) params.set("resize", options.transform.resize);
|
|
1092
|
+
if (options.transform.quality) params.set("quality", String(options.transform.quality));
|
|
1093
|
+
if (options.transform.format) params.set("format", options.transform.format);
|
|
1094
|
+
}
|
|
1095
|
+
const qs = params.toString();
|
|
1096
|
+
const url = `${this.storageUrl}/object/${this.bucketId}/${path}${qs ? "?" + qs : ""}`;
|
|
1097
|
+
const fetchFn = this.customFetch || (globalThis.fetch ?? (await import("cross-fetch")).default);
|
|
1098
|
+
const headers = {
|
|
1099
|
+
"apikey": this.apiKey
|
|
1100
|
+
};
|
|
1101
|
+
const token = this.getAccessToken();
|
|
1102
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1103
|
+
else headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1104
|
+
const resp = await fetchFn(url, { headers });
|
|
1105
|
+
if (!resp.ok) {
|
|
1106
|
+
let errorBody;
|
|
1107
|
+
try {
|
|
1108
|
+
errorBody = await resp.json();
|
|
1109
|
+
} catch {
|
|
1110
|
+
errorBody = { message: `Download failed with status ${resp.status}` };
|
|
1111
|
+
}
|
|
1112
|
+
return { data: null, error: errorBody };
|
|
1113
|
+
}
|
|
1114
|
+
const blob = await resp.blob();
|
|
1115
|
+
return { data: blob, error: null };
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* List objects in the bucket.
|
|
1119
|
+
*/
|
|
1120
|
+
async list(path, options) {
|
|
1121
|
+
const body = {
|
|
1122
|
+
prefix: path || ""
|
|
1123
|
+
};
|
|
1124
|
+
if (options?.limit) body.limit = options.limit;
|
|
1125
|
+
if (options?.offset) body.offset = options.offset;
|
|
1126
|
+
if (options?.sortBy) body.sortBy = options.sortBy;
|
|
1127
|
+
const resp = await fetchWithAuth(`${this.storageUrl}/object/list/${this.bucketId}`, {
|
|
1128
|
+
method: "POST",
|
|
1129
|
+
headers: { "Content-Type": "application/json" },
|
|
1130
|
+
body: JSON.stringify(body),
|
|
1131
|
+
apiKey: this.apiKey,
|
|
1132
|
+
accessToken: this.getAccessToken(),
|
|
1133
|
+
customFetch: this.customFetch
|
|
1134
|
+
});
|
|
1135
|
+
return handleResponse(resp);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Move a file to a new path within the same bucket.
|
|
1139
|
+
*/
|
|
1140
|
+
async move(fromPath, toPath) {
|
|
1141
|
+
const resp = await fetchWithAuth(`${this.storageUrl}/object/move`, {
|
|
1142
|
+
method: "POST",
|
|
1143
|
+
headers: { "Content-Type": "application/json" },
|
|
1144
|
+
body: JSON.stringify({
|
|
1145
|
+
bucketId: this.bucketId,
|
|
1146
|
+
sourceKey: fromPath,
|
|
1147
|
+
destinationKey: toPath
|
|
1148
|
+
}),
|
|
1149
|
+
apiKey: this.apiKey,
|
|
1150
|
+
accessToken: this.getAccessToken(),
|
|
1151
|
+
customFetch: this.customFetch
|
|
1152
|
+
});
|
|
1153
|
+
return handleResponse(resp);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Copy a file to a new path within the same bucket.
|
|
1157
|
+
*/
|
|
1158
|
+
async copy(fromPath, toPath) {
|
|
1159
|
+
const resp = await fetchWithAuth(`${this.storageUrl}/object/copy`, {
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
headers: { "Content-Type": "application/json" },
|
|
1162
|
+
body: JSON.stringify({
|
|
1163
|
+
bucketId: this.bucketId,
|
|
1164
|
+
sourceKey: fromPath,
|
|
1165
|
+
destinationKey: toPath
|
|
1166
|
+
}),
|
|
1167
|
+
apiKey: this.apiKey,
|
|
1168
|
+
accessToken: this.getAccessToken(),
|
|
1169
|
+
customFetch: this.customFetch
|
|
1170
|
+
});
|
|
1171
|
+
return handleResponse(resp);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Remove one or more files from the bucket.
|
|
1175
|
+
*/
|
|
1176
|
+
async remove(paths) {
|
|
1177
|
+
const resp = await fetchWithAuth(`${this.storageUrl}/object/${this.bucketId}`, {
|
|
1178
|
+
method: "DELETE",
|
|
1179
|
+
headers: { "Content-Type": "application/json" },
|
|
1180
|
+
body: JSON.stringify({ prefixes: paths }),
|
|
1181
|
+
apiKey: this.apiKey,
|
|
1182
|
+
accessToken: this.getAccessToken(),
|
|
1183
|
+
customFetch: this.customFetch
|
|
1184
|
+
});
|
|
1185
|
+
return handleResponse(resp);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Create a signed URL for temporary access to a private file.
|
|
1189
|
+
*/
|
|
1190
|
+
async createSignedUrl(path, expiresIn, options) {
|
|
1191
|
+
const body = {
|
|
1192
|
+
expiresIn
|
|
1193
|
+
};
|
|
1194
|
+
if (options?.download) {
|
|
1195
|
+
body.download = typeof options.download === "string" ? options.download : true;
|
|
1196
|
+
}
|
|
1197
|
+
if (options?.transform) body.transform = options.transform;
|
|
1198
|
+
const resp = await fetchWithAuth(`${this.storageUrl}/object/sign/${this.bucketId}/${path}`, {
|
|
1199
|
+
method: "POST",
|
|
1200
|
+
headers: { "Content-Type": "application/json" },
|
|
1201
|
+
body: JSON.stringify(body),
|
|
1202
|
+
apiKey: this.apiKey,
|
|
1203
|
+
accessToken: this.getAccessToken(),
|
|
1204
|
+
customFetch: this.customFetch
|
|
1205
|
+
});
|
|
1206
|
+
const result = await handleResponse(resp);
|
|
1207
|
+
if (result.error) return { data: null, error: result.error };
|
|
1208
|
+
const signedUrl = result.data?.signedURL || result.data?.signedUrl;
|
|
1209
|
+
return {
|
|
1210
|
+
data: {
|
|
1211
|
+
signedUrl: signedUrl?.startsWith("http") ? signedUrl : `${this.storageUrl}${signedUrl}`,
|
|
1212
|
+
path,
|
|
1213
|
+
token: result.data?.token || ""
|
|
1214
|
+
},
|
|
1215
|
+
error: null
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Create signed URLs for multiple files.
|
|
1220
|
+
*/
|
|
1221
|
+
async createSignedUrls(paths, expiresIn) {
|
|
1222
|
+
const resp = await fetchWithAuth(`${this.storageUrl}/object/sign/${this.bucketId}`, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: { "Content-Type": "application/json" },
|
|
1225
|
+
body: JSON.stringify({ expiresIn, paths }),
|
|
1226
|
+
apiKey: this.apiKey,
|
|
1227
|
+
accessToken: this.getAccessToken(),
|
|
1228
|
+
customFetch: this.customFetch
|
|
1229
|
+
});
|
|
1230
|
+
const result = await handleResponse(resp);
|
|
1231
|
+
if (result.error) return { data: null, error: result.error };
|
|
1232
|
+
const urls = (result.data || []).map((item) => ({
|
|
1233
|
+
signedUrl: item.signedURL?.startsWith("http") ? item.signedURL : `${this.storageUrl}${item.signedURL}`,
|
|
1234
|
+
path: item.path,
|
|
1235
|
+
token: item.token || ""
|
|
1236
|
+
}));
|
|
1237
|
+
return { data: urls, error: null };
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Get the public URL for a file in a public bucket.
|
|
1241
|
+
*/
|
|
1242
|
+
getPublicUrl(path, options) {
|
|
1243
|
+
const params = new URLSearchParams();
|
|
1244
|
+
if (options?.download) {
|
|
1245
|
+
params.set("download", typeof options.download === "string" ? options.download : "");
|
|
1246
|
+
}
|
|
1247
|
+
if (options?.transform) {
|
|
1248
|
+
if (options.transform.width) params.set("width", String(options.transform.width));
|
|
1249
|
+
if (options.transform.height) params.set("height", String(options.transform.height));
|
|
1250
|
+
if (options.transform.resize) params.set("resize", options.transform.resize);
|
|
1251
|
+
if (options.transform.quality) params.set("quality", String(options.transform.quality));
|
|
1252
|
+
if (options.transform.format) params.set("format", options.transform.format);
|
|
1253
|
+
}
|
|
1254
|
+
const qs = params.toString();
|
|
1255
|
+
const publicUrl = `${this.storageUrl}/object/public/${this.bucketId}/${path}${qs ? "?" + qs : ""}`;
|
|
1256
|
+
return { data: { publicUrl } };
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
// src/lib/AfribaseFunctionsClient.ts
|
|
1261
|
+
var AfribaseFunctionsClient = class {
|
|
1262
|
+
constructor(options) {
|
|
1263
|
+
this.url = options.url.replace(/\/$/, "");
|
|
1264
|
+
this.apiKey = options.apiKey;
|
|
1265
|
+
this.getAccessToken = options.getAccessToken;
|
|
1266
|
+
this.customFetch = options.customFetch;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Invoke an edge function by name.
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* const { data, error } = await client.functions.invoke('hello-world', {
|
|
1273
|
+
* body: { name: 'Afribase' },
|
|
1274
|
+
* });
|
|
1275
|
+
*/
|
|
1276
|
+
async invoke(functionName, options) {
|
|
1277
|
+
const method = options?.method || "POST";
|
|
1278
|
+
const headers = {
|
|
1279
|
+
...options?.headers
|
|
1280
|
+
};
|
|
1281
|
+
let body = void 0;
|
|
1282
|
+
if (options?.body !== void 0) {
|
|
1283
|
+
if (typeof options.body === "string" || options.body instanceof Blob || options.body instanceof ArrayBuffer || options.body instanceof FormData) {
|
|
1284
|
+
body = options.body;
|
|
1285
|
+
} else {
|
|
1286
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
1287
|
+
body = JSON.stringify(options.body);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (options?.region) {
|
|
1291
|
+
headers["x-region"] = options.region;
|
|
1292
|
+
}
|
|
1293
|
+
const resp = await fetchWithAuth(`${this.url}/${functionName}`, {
|
|
1294
|
+
method,
|
|
1295
|
+
headers,
|
|
1296
|
+
body,
|
|
1297
|
+
apiKey: this.apiKey,
|
|
1298
|
+
accessToken: this.getAccessToken(),
|
|
1299
|
+
customFetch: this.customFetch
|
|
1300
|
+
});
|
|
1301
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
1302
|
+
if (contentType.includes("application/json")) {
|
|
1303
|
+
return handleResponse(resp);
|
|
1304
|
+
}
|
|
1305
|
+
if (!resp.ok) {
|
|
1306
|
+
const text2 = await resp.text();
|
|
1307
|
+
return {
|
|
1308
|
+
data: null,
|
|
1309
|
+
error: { message: text2, status: resp.status }
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
const text = await resp.text();
|
|
1313
|
+
return { data: text, error: null };
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// src/lib/AfribaseQueryBuilder.ts
|
|
1318
|
+
var AfribaseQueryBuilder = class {
|
|
1319
|
+
constructor(url, options) {
|
|
1320
|
+
this._method = null;
|
|
1321
|
+
this._body = void 0;
|
|
1322
|
+
this._isSingle = false;
|
|
1323
|
+
this._isMaybeSingle = false;
|
|
1324
|
+
this._isHead = false;
|
|
1325
|
+
this._countType = null;
|
|
1326
|
+
this._url = url;
|
|
1327
|
+
this._apiKey = options.apiKey;
|
|
1328
|
+
this._accessToken = options.accessToken;
|
|
1329
|
+
this._customFetch = options.customFetch;
|
|
1330
|
+
this._query = new URLSearchParams();
|
|
1331
|
+
this._headers = {
|
|
1332
|
+
"Content-Type": "application/json",
|
|
1333
|
+
"Prefer": "return=representation"
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1337
|
+
// CRUD Operations
|
|
1338
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1339
|
+
/**
|
|
1340
|
+
* Perform a SELECT query.
|
|
1341
|
+
*/
|
|
1342
|
+
select(columns = "*", options) {
|
|
1343
|
+
this._method = "GET";
|
|
1344
|
+
this._query.set("select", columns);
|
|
1345
|
+
if (options?.count) {
|
|
1346
|
+
this._countType = options.count;
|
|
1347
|
+
this._headers["Prefer"] = `count=${options.count}`;
|
|
1348
|
+
}
|
|
1349
|
+
if (options?.head) {
|
|
1350
|
+
this._isHead = true;
|
|
1351
|
+
this._method = "HEAD";
|
|
1352
|
+
}
|
|
1353
|
+
return this;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Perform an INSERT.
|
|
1357
|
+
*/
|
|
1358
|
+
insert(values, options) {
|
|
1359
|
+
this._method = "POST";
|
|
1360
|
+
this._body = values;
|
|
1361
|
+
const prefer = ["return=representation"];
|
|
1362
|
+
if (options?.count) {
|
|
1363
|
+
this._countType = options.count;
|
|
1364
|
+
prefer.push(`count=${options.count}`);
|
|
1365
|
+
}
|
|
1366
|
+
if (options?.defaultToNull === false) prefer.push("missing=default");
|
|
1367
|
+
this._headers["Prefer"] = prefer.join(",");
|
|
1368
|
+
return this;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Perform an UPSERT (insert or update on conflict).
|
|
1372
|
+
*/
|
|
1373
|
+
upsert(values, options) {
|
|
1374
|
+
this._method = "POST";
|
|
1375
|
+
this._body = values;
|
|
1376
|
+
const prefer = ["return=representation", "resolution=merge-duplicates"];
|
|
1377
|
+
if (options?.ignoreDuplicates) {
|
|
1378
|
+
prefer[1] = "resolution=ignore-duplicates";
|
|
1379
|
+
}
|
|
1380
|
+
if (options?.count) {
|
|
1381
|
+
this._countType = options.count;
|
|
1382
|
+
prefer.push(`count=${options.count}`);
|
|
1383
|
+
}
|
|
1384
|
+
if (options?.defaultToNull === false) prefer.push("missing=default");
|
|
1385
|
+
if (options?.onConflict) this._query.set("on_conflict", options.onConflict);
|
|
1386
|
+
this._headers["Prefer"] = prefer.join(",");
|
|
1387
|
+
return this;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Perform an UPDATE (must be combined with filters).
|
|
1391
|
+
*/
|
|
1392
|
+
update(values, options) {
|
|
1393
|
+
this._method = "PATCH";
|
|
1394
|
+
this._body = values;
|
|
1395
|
+
const prefer = ["return=representation"];
|
|
1396
|
+
if (options?.count) {
|
|
1397
|
+
this._countType = options.count;
|
|
1398
|
+
prefer.push(`count=${options.count}`);
|
|
1399
|
+
}
|
|
1400
|
+
this._headers["Prefer"] = prefer.join(",");
|
|
1401
|
+
return this;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Perform a DELETE (must be combined with filters).
|
|
1405
|
+
*/
|
|
1406
|
+
delete(options) {
|
|
1407
|
+
this._method = "DELETE";
|
|
1408
|
+
const prefer = ["return=representation"];
|
|
1409
|
+
if (options?.count) {
|
|
1410
|
+
this._countType = options.count;
|
|
1411
|
+
prefer.push(`count=${options.count}`);
|
|
1412
|
+
}
|
|
1413
|
+
this._headers["Prefer"] = prefer.join(",");
|
|
1414
|
+
return this;
|
|
1415
|
+
}
|
|
1416
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1417
|
+
// Filters
|
|
1418
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1419
|
+
eq(column, value) {
|
|
1420
|
+
this._query.append(column, `eq.${value}`);
|
|
1421
|
+
return this;
|
|
1422
|
+
}
|
|
1423
|
+
neq(column, value) {
|
|
1424
|
+
this._query.append(column, `neq.${value}`);
|
|
1425
|
+
return this;
|
|
1426
|
+
}
|
|
1427
|
+
gt(column, value) {
|
|
1428
|
+
this._query.append(column, `gt.${value}`);
|
|
1429
|
+
return this;
|
|
1430
|
+
}
|
|
1431
|
+
gte(column, value) {
|
|
1432
|
+
this._query.append(column, `gte.${value}`);
|
|
1433
|
+
return this;
|
|
1434
|
+
}
|
|
1435
|
+
lt(column, value) {
|
|
1436
|
+
this._query.append(column, `lt.${value}`);
|
|
1437
|
+
return this;
|
|
1438
|
+
}
|
|
1439
|
+
lte(column, value) {
|
|
1440
|
+
this._query.append(column, `lte.${value}`);
|
|
1441
|
+
return this;
|
|
1442
|
+
}
|
|
1443
|
+
like(column, pattern) {
|
|
1444
|
+
this._query.append(column, `like.${pattern}`);
|
|
1445
|
+
return this;
|
|
1446
|
+
}
|
|
1447
|
+
ilike(column, pattern) {
|
|
1448
|
+
this._query.append(column, `ilike.${pattern}`);
|
|
1449
|
+
return this;
|
|
1450
|
+
}
|
|
1451
|
+
is(column, value) {
|
|
1452
|
+
this._query.append(column, `is.${value}`);
|
|
1453
|
+
return this;
|
|
1454
|
+
}
|
|
1455
|
+
in(column, values) {
|
|
1456
|
+
const cleanedValues = values.map(
|
|
1457
|
+
(v) => typeof v === "string" ? `"${v}"` : v
|
|
1458
|
+
);
|
|
1459
|
+
this._query.append(column, `in.(${cleanedValues.join(",")})`);
|
|
1460
|
+
return this;
|
|
1461
|
+
}
|
|
1462
|
+
contains(column, value) {
|
|
1463
|
+
if (Array.isArray(value)) {
|
|
1464
|
+
this._query.append(column, `cs.{${value.join(",")}}`);
|
|
1465
|
+
} else if (typeof value === "object") {
|
|
1466
|
+
this._query.append(column, `cs.${JSON.stringify(value)}`);
|
|
1467
|
+
} else {
|
|
1468
|
+
this._query.append(column, `cs.${value}`);
|
|
1469
|
+
}
|
|
1470
|
+
return this;
|
|
1471
|
+
}
|
|
1472
|
+
containedBy(column, value) {
|
|
1473
|
+
if (Array.isArray(value)) {
|
|
1474
|
+
this._query.append(column, `cd.{${value.join(",")}}`);
|
|
1475
|
+
} else if (typeof value === "object") {
|
|
1476
|
+
this._query.append(column, `cd.${JSON.stringify(value)}`);
|
|
1477
|
+
} else {
|
|
1478
|
+
this._query.append(column, `cd.${value}`);
|
|
1479
|
+
}
|
|
1480
|
+
return this;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Full text search using to_tsquery.
|
|
1484
|
+
*/
|
|
1485
|
+
textSearch(column, query, options) {
|
|
1486
|
+
let op = "fts";
|
|
1487
|
+
if (options?.type === "plain") op = "plfts";
|
|
1488
|
+
else if (options?.type === "phrase") op = "phfts";
|
|
1489
|
+
else if (options?.type === "websearch") op = "wfts";
|
|
1490
|
+
const configStr = options?.config ? `(${options.config})` : "";
|
|
1491
|
+
this._query.append(column, `${op}${configStr}.${query}`);
|
|
1492
|
+
return this;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Negate a filter.
|
|
1496
|
+
*/
|
|
1497
|
+
not(column, operator, value) {
|
|
1498
|
+
this._query.append(column, `not.${operator}.${value}`);
|
|
1499
|
+
return this;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Combine filters with OR.
|
|
1503
|
+
*/
|
|
1504
|
+
or(filters, options) {
|
|
1505
|
+
const key = options?.foreignTable ? `${options.foreignTable}.or` : "or";
|
|
1506
|
+
this._query.append(key, `(${filters})`);
|
|
1507
|
+
return this;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Generic filter — any PostgREST operator.
|
|
1511
|
+
*/
|
|
1512
|
+
filter(column, operator, value) {
|
|
1513
|
+
this._query.append(column, `${operator}.${value}`);
|
|
1514
|
+
return this;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Match multiple column values (shorthand for multiple .eq).
|
|
1518
|
+
*/
|
|
1519
|
+
match(query) {
|
|
1520
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1521
|
+
this._query.append(key, `eq.${value}`);
|
|
1522
|
+
}
|
|
1523
|
+
return this;
|
|
1524
|
+
}
|
|
1525
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1526
|
+
// Modifiers
|
|
1527
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1528
|
+
/**
|
|
1529
|
+
* Order results.
|
|
1530
|
+
*/
|
|
1531
|
+
order(column, options) {
|
|
1532
|
+
const direction = options?.ascending === false ? "desc" : "asc";
|
|
1533
|
+
const nulls = options?.nullsFirst ? ".nullsfirst" : ".nullslast";
|
|
1534
|
+
const key = options?.foreignTable ? `${options.foreignTable}.order` : "order";
|
|
1535
|
+
this._query.append(key, `${column}.${direction}${nulls}`);
|
|
1536
|
+
return this;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Limit the number of results.
|
|
1540
|
+
*/
|
|
1541
|
+
limit(count, options) {
|
|
1542
|
+
const key = options?.foreignTable ? `${options.foreignTable}.limit` : "limit";
|
|
1543
|
+
this._query.set(key, String(count));
|
|
1544
|
+
return this;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Paginate with offset.
|
|
1548
|
+
*/
|
|
1549
|
+
range(from, to, options) {
|
|
1550
|
+
const key = options?.foreignTable ? `${options.foreignTable}.offset` : "offset";
|
|
1551
|
+
this._query.set(key, String(from));
|
|
1552
|
+
const limitKey = options?.foreignTable ? `${options.foreignTable}.limit` : "limit";
|
|
1553
|
+
this._query.set(limitKey, String(to - from + 1));
|
|
1554
|
+
this._headers["Range"] = `${from}-${to}`;
|
|
1555
|
+
this._headers["Range-Unit"] = "items";
|
|
1556
|
+
return this;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Return a single row (throws if 0 or 2+ rows).
|
|
1560
|
+
*/
|
|
1561
|
+
single() {
|
|
1562
|
+
this._isSingle = true;
|
|
1563
|
+
this._headers["Accept"] = "application/vnd.pgrst.object+json";
|
|
1564
|
+
return this;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Return at most one row (returns null if 0 rows).
|
|
1568
|
+
*/
|
|
1569
|
+
maybeSingle() {
|
|
1570
|
+
this._isMaybeSingle = true;
|
|
1571
|
+
this._headers["Accept"] = "application/vnd.pgrst.object+json";
|
|
1572
|
+
return this;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Return as CSV.
|
|
1576
|
+
*/
|
|
1577
|
+
csv() {
|
|
1578
|
+
this._headers["Accept"] = "text/csv";
|
|
1579
|
+
return this;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Return a GeoJSON response.
|
|
1583
|
+
*/
|
|
1584
|
+
geojson() {
|
|
1585
|
+
this._headers["Accept"] = "application/geo+json";
|
|
1586
|
+
return this;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Limit the fields returned (PostgREST explain).
|
|
1590
|
+
*/
|
|
1591
|
+
explain(options) {
|
|
1592
|
+
const parts = ["application/vnd.pgrst.plan"];
|
|
1593
|
+
if (options?.analyze) parts.push("analyze");
|
|
1594
|
+
if (options?.verbose) parts.push("verbose");
|
|
1595
|
+
if (options?.settings) parts.push("settings");
|
|
1596
|
+
if (options?.buffers) parts.push("buffers");
|
|
1597
|
+
if (options?.format) parts.push(options.format);
|
|
1598
|
+
this._headers["Accept"] = parts.join("+");
|
|
1599
|
+
return this;
|
|
1600
|
+
}
|
|
1601
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1602
|
+
// Execute
|
|
1603
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1604
|
+
/**
|
|
1605
|
+
* Execute the query and return the results.
|
|
1606
|
+
*/
|
|
1607
|
+
async then(onfulfilled, onrejected) {
|
|
1608
|
+
let res;
|
|
1609
|
+
try {
|
|
1610
|
+
res = await this._execute();
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
if (onrejected) return Promise.resolve(onrejected(err));
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
if (onfulfilled) return Promise.resolve(onfulfilled(res));
|
|
1616
|
+
return res;
|
|
1617
|
+
}
|
|
1618
|
+
async _execute() {
|
|
1619
|
+
const method = this._method || "GET";
|
|
1620
|
+
const queryString = this._query.toString();
|
|
1621
|
+
const requestUrl = queryString ? `${this._url}?${queryString}` : this._url;
|
|
1622
|
+
const resp = await fetchWithAuth(requestUrl, {
|
|
1623
|
+
method,
|
|
1624
|
+
headers: this._headers,
|
|
1625
|
+
body: this._body ? JSON.stringify(this._body) : void 0,
|
|
1626
|
+
apiKey: this._apiKey,
|
|
1627
|
+
accessToken: this._accessToken,
|
|
1628
|
+
customFetch: this._customFetch
|
|
1629
|
+
});
|
|
1630
|
+
let count = null;
|
|
1631
|
+
if (this._countType) {
|
|
1632
|
+
const contentRange = resp.headers.get("content-range");
|
|
1633
|
+
if (contentRange) {
|
|
1634
|
+
const match2 = contentRange.match(/\/(\d+)/);
|
|
1635
|
+
if (match2) count = parseInt(match2[1], 10);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (!resp.ok) {
|
|
1639
|
+
let errorBody;
|
|
1640
|
+
try {
|
|
1641
|
+
errorBody = await resp.json();
|
|
1642
|
+
} catch {
|
|
1643
|
+
errorBody = { message: await resp.text() };
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
data: null,
|
|
1647
|
+
error: {
|
|
1648
|
+
message: errorBody.message || `Request failed with status ${resp.status}`,
|
|
1649
|
+
details: errorBody.details || "",
|
|
1650
|
+
hint: errorBody.hint || "",
|
|
1651
|
+
code: errorBody.code || String(resp.status)
|
|
1652
|
+
},
|
|
1653
|
+
count,
|
|
1654
|
+
status: resp.status,
|
|
1655
|
+
statusText: resp.statusText
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (this._isHead || method === "HEAD") {
|
|
1659
|
+
return { data: null, error: null, count, status: resp.status, statusText: resp.statusText };
|
|
1660
|
+
}
|
|
1661
|
+
if (resp.status === 204) {
|
|
1662
|
+
return { data: null, error: null, count, status: resp.status, statusText: resp.statusText };
|
|
1663
|
+
}
|
|
1664
|
+
let data;
|
|
1665
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
1666
|
+
if (contentType.includes("text/csv") || contentType.includes("geo+json") || contentType.includes("pgrst.plan")) {
|
|
1667
|
+
data = await resp.text();
|
|
1668
|
+
} else {
|
|
1669
|
+
data = await resp.json();
|
|
1670
|
+
}
|
|
1671
|
+
if (this._isMaybeSingle && (resp.status === 406 || data === null)) {
|
|
1672
|
+
return { data: null, error: null, count, status: 200, statusText: "OK" };
|
|
1673
|
+
}
|
|
1674
|
+
return { data, error: null, count, status: resp.status, statusText: resp.statusText };
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
var AfribaseRpcBuilder = class {
|
|
1678
|
+
constructor(url, fnName, params, options) {
|
|
1679
|
+
this._isSingle = false;
|
|
1680
|
+
this._countType = null;
|
|
1681
|
+
this._url = `${url}/rpc/${fnName}`;
|
|
1682
|
+
this._apiKey = options.apiKey;
|
|
1683
|
+
this._accessToken = options.accessToken;
|
|
1684
|
+
this._customFetch = options.customFetch;
|
|
1685
|
+
this._params = params;
|
|
1686
|
+
this._query = new URLSearchParams();
|
|
1687
|
+
this._headers = {
|
|
1688
|
+
"Content-Type": "application/json",
|
|
1689
|
+
"Prefer": "return=representation"
|
|
1690
|
+
};
|
|
1691
|
+
this._method = options.get ? "GET" : "POST";
|
|
1692
|
+
if (options.count) {
|
|
1693
|
+
this._countType = options.count;
|
|
1694
|
+
this._headers["Prefer"] = `count=${options.count}`;
|
|
1695
|
+
}
|
|
1696
|
+
if (this._method === "GET" && params) {
|
|
1697
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1698
|
+
this._query.set(key, String(value));
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
single() {
|
|
1703
|
+
this._isSingle = true;
|
|
1704
|
+
this._headers["Accept"] = "application/vnd.pgrst.object+json";
|
|
1705
|
+
return this;
|
|
1706
|
+
}
|
|
1707
|
+
maybeSingle() {
|
|
1708
|
+
this._headers["Accept"] = "application/vnd.pgrst.object+json";
|
|
1709
|
+
return this;
|
|
1710
|
+
}
|
|
1711
|
+
select(columns = "*") {
|
|
1712
|
+
this._query.set("select", columns);
|
|
1713
|
+
return this;
|
|
1714
|
+
}
|
|
1715
|
+
order(column, options) {
|
|
1716
|
+
const direction = options?.ascending === false ? "desc" : "asc";
|
|
1717
|
+
this._query.append("order", `${column}.${direction}`);
|
|
1718
|
+
return this;
|
|
1719
|
+
}
|
|
1720
|
+
limit(count) {
|
|
1721
|
+
this._query.set("limit", String(count));
|
|
1722
|
+
return this;
|
|
1723
|
+
}
|
|
1724
|
+
range(from, to) {
|
|
1725
|
+
this._query.set("offset", String(from));
|
|
1726
|
+
this._query.set("limit", String(to - from + 1));
|
|
1727
|
+
this._headers["Range"] = `${from}-${to}`;
|
|
1728
|
+
this._headers["Range-Unit"] = "items";
|
|
1729
|
+
return this;
|
|
1730
|
+
}
|
|
1731
|
+
eq(column, value) {
|
|
1732
|
+
this._query.append(column, `eq.${value}`);
|
|
1733
|
+
return this;
|
|
1734
|
+
}
|
|
1735
|
+
filter(column, operator, value) {
|
|
1736
|
+
this._query.append(column, `${operator}.${value}`);
|
|
1737
|
+
return this;
|
|
1738
|
+
}
|
|
1739
|
+
async then(onfulfilled, onrejected) {
|
|
1740
|
+
let res;
|
|
1741
|
+
try {
|
|
1742
|
+
res = await this._execute();
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
if (onrejected) return Promise.resolve(onrejected(err));
|
|
1745
|
+
throw err;
|
|
1746
|
+
}
|
|
1747
|
+
if (onfulfilled) return Promise.resolve(onfulfilled(res));
|
|
1748
|
+
return res;
|
|
1749
|
+
}
|
|
1750
|
+
async _execute() {
|
|
1751
|
+
const queryString = this._query.toString();
|
|
1752
|
+
const requestUrl = queryString ? `${this._url}?${queryString}` : this._url;
|
|
1753
|
+
const resp = await fetchWithAuth(requestUrl, {
|
|
1754
|
+
method: this._method,
|
|
1755
|
+
headers: this._headers,
|
|
1756
|
+
body: this._method === "POST" ? JSON.stringify(this._params) : void 0,
|
|
1757
|
+
apiKey: this._apiKey,
|
|
1758
|
+
accessToken: this._accessToken,
|
|
1759
|
+
customFetch: this._customFetch
|
|
1760
|
+
});
|
|
1761
|
+
let count = null;
|
|
1762
|
+
if (this._countType) {
|
|
1763
|
+
const contentRange = resp.headers.get("content-range");
|
|
1764
|
+
if (contentRange) {
|
|
1765
|
+
const match2 = contentRange.match(/\/(\d+)/);
|
|
1766
|
+
if (match2) count = parseInt(match2[1], 10);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (!resp.ok) {
|
|
1770
|
+
let errorBody;
|
|
1771
|
+
try {
|
|
1772
|
+
errorBody = await resp.json();
|
|
1773
|
+
} catch {
|
|
1774
|
+
errorBody = { message: await resp.text() };
|
|
1775
|
+
}
|
|
1776
|
+
return {
|
|
1777
|
+
data: null,
|
|
1778
|
+
error: {
|
|
1779
|
+
message: errorBody.message || `RPC failed with status ${resp.status}`,
|
|
1780
|
+
details: errorBody.details || "",
|
|
1781
|
+
hint: errorBody.hint || "",
|
|
1782
|
+
code: errorBody.code || String(resp.status)
|
|
1783
|
+
},
|
|
1784
|
+
count,
|
|
1785
|
+
status: resp.status,
|
|
1786
|
+
statusText: resp.statusText
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
if (resp.status === 204) {
|
|
1790
|
+
return { data: null, error: null, count, status: 204, statusText: "No Content" };
|
|
1791
|
+
}
|
|
1792
|
+
const data = await resp.json();
|
|
1793
|
+
return { data, error: null, count, status: resp.status, statusText: resp.statusText };
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/lib/modular.ts
|
|
1798
|
+
async function db(builder, ...ops) {
|
|
1799
|
+
let current = builder;
|
|
1800
|
+
for (const op of ops) {
|
|
1801
|
+
current = op(current);
|
|
1802
|
+
}
|
|
1803
|
+
return current;
|
|
1804
|
+
}
|
|
1805
|
+
var select = (columns = "*", options) => (b) => b.select(columns, options);
|
|
1806
|
+
var insert = (values, options) => (b) => b.insert(values, options);
|
|
1807
|
+
var update = (values, options) => (b) => b.update(values, options);
|
|
1808
|
+
var upsert = (values, options) => (b) => b.upsert(values, options);
|
|
1809
|
+
var del = (options) => (b) => b.delete(options);
|
|
1810
|
+
var eq = (column, value) => (b) => b.eq(column, value);
|
|
1811
|
+
var neq = (column, value) => (b) => b.neq(column, value);
|
|
1812
|
+
var gt = (column, value) => (b) => b.gt(column, value);
|
|
1813
|
+
var gte = (column, value) => (b) => b.gte(column, value);
|
|
1814
|
+
var lt = (column, value) => (b) => b.lt(column, value);
|
|
1815
|
+
var lte = (column, value) => (b) => b.lte(column, value);
|
|
1816
|
+
var like = (column, pattern) => (b) => b.like(column, pattern);
|
|
1817
|
+
var ilike = (column, pattern) => (b) => b.ilike(column, pattern);
|
|
1818
|
+
var is = (column, value) => (b) => b.is(column, value);
|
|
1819
|
+
var isIn = (column, values) => (b) => b.in(column, values);
|
|
1820
|
+
var contains = (column, value) => (b) => b.contains(column, value);
|
|
1821
|
+
var containedBy = (column, value) => (b) => b.containedBy(column, value);
|
|
1822
|
+
var textSearch = (column, query, options) => (b) => b.textSearch(column, query, options);
|
|
1823
|
+
var not = (column, operator, value) => (b) => b.not(column, operator, value);
|
|
1824
|
+
var or = (filters, options) => (b) => b.or(filters, options);
|
|
1825
|
+
var match = (query) => (b) => b.match(query);
|
|
1826
|
+
var order = (column, options) => (b) => b.order(column, options);
|
|
1827
|
+
var limit = (count, options) => (b) => b.limit(count, options);
|
|
1828
|
+
var range = (from, to, options) => (b) => b.range(from, to, options);
|
|
1829
|
+
var single = () => (b) => b.single();
|
|
1830
|
+
var maybeSingle = () => (b) => b.maybeSingle();
|
|
1831
|
+
var csv = () => (b) => b.csv();
|
|
1832
|
+
var geojson = () => (b) => b.geojson();
|
|
1833
|
+
var explain = (options) => (b) => b.explain(options);
|
|
1834
|
+
|
|
1835
|
+
// src/index.ts
|
|
1836
|
+
var AfribaseClient = class {
|
|
1837
|
+
constructor(baseUrl, anonKey, options) {
|
|
1838
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1839
|
+
this.anonKey = anonKey;
|
|
1840
|
+
this.customFetch = options?.fetch;
|
|
1841
|
+
const authUrl = options?.authUrl || `${this.baseUrl}/auth/v1`;
|
|
1842
|
+
this.restUrl = options?.restUrl || `${this.baseUrl}/rest/v1`;
|
|
1843
|
+
const realtimeUrl = options?.realtimeUrl || this.baseUrl;
|
|
1844
|
+
const storageUrl = options?.storageUrl || `${this.baseUrl}/storage/v1`;
|
|
1845
|
+
const functionsUrl = options?.functionsUrl || `${this.baseUrl}/functions/v1`;
|
|
1846
|
+
this.auth = new AfribaseAuthClient({
|
|
1847
|
+
url: authUrl,
|
|
1848
|
+
apiKey: anonKey,
|
|
1849
|
+
autoRefreshToken: options?.autoRefreshToken ?? true,
|
|
1850
|
+
persistSession: options?.persistSession ?? true,
|
|
1851
|
+
customFetch: this.customFetch
|
|
1852
|
+
});
|
|
1853
|
+
this.realtimeClient = new AfribaseRealtimeClient({
|
|
1854
|
+
url: realtimeUrl,
|
|
1855
|
+
apiKey: anonKey,
|
|
1856
|
+
accessToken: this.auth.accessToken
|
|
1857
|
+
});
|
|
1858
|
+
this.storage = new AfribaseStorageClient({
|
|
1859
|
+
url: storageUrl,
|
|
1860
|
+
apiKey: anonKey,
|
|
1861
|
+
getAccessToken: () => this.auth.accessToken,
|
|
1862
|
+
customFetch: this.customFetch
|
|
1863
|
+
});
|
|
1864
|
+
this.functions = new AfribaseFunctionsClient({
|
|
1865
|
+
url: functionsUrl,
|
|
1866
|
+
apiKey: anonKey,
|
|
1867
|
+
getAccessToken: () => this.auth.accessToken,
|
|
1868
|
+
customFetch: this.customFetch
|
|
1869
|
+
});
|
|
1870
|
+
this.auth.onAuthStateChange((_event, session) => {
|
|
1871
|
+
this.realtimeClient.setAuth(session?.access_token ?? null);
|
|
1872
|
+
});
|
|
1873
|
+
this.auth.initialize();
|
|
1874
|
+
}
|
|
1875
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1876
|
+
// Database (PostgREST)
|
|
1877
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1878
|
+
/**
|
|
1879
|
+
* Query a table via PostgREST.
|
|
1880
|
+
*
|
|
1881
|
+
* @example
|
|
1882
|
+
* const { data } = await afribase.from('users').select('*').eq('id', 1);
|
|
1883
|
+
* const { data } = await afribase.from('posts').select('*, comments(*)').order('created_at', { ascending: false }).limit(10);
|
|
1884
|
+
*/
|
|
1885
|
+
from(table) {
|
|
1886
|
+
return new AfribaseQueryBuilder(`${this.restUrl}/${table}`, {
|
|
1887
|
+
apiKey: this.anonKey,
|
|
1888
|
+
accessToken: this.auth.accessToken,
|
|
1889
|
+
customFetch: this.customFetch
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Call a Postgres function (RPC).
|
|
1894
|
+
*
|
|
1895
|
+
* @example
|
|
1896
|
+
* const { data } = await afribase.rpc('get_top_users', { limit_count: 10 });
|
|
1897
|
+
*/
|
|
1898
|
+
rpc(fn, params = {}, options) {
|
|
1899
|
+
return new AfribaseRpcBuilder(this.restUrl, fn, params, {
|
|
1900
|
+
apiKey: this.anonKey,
|
|
1901
|
+
accessToken: this.auth.accessToken,
|
|
1902
|
+
customFetch: this.customFetch,
|
|
1903
|
+
head: options?.head,
|
|
1904
|
+
count: options?.count,
|
|
1905
|
+
get: options?.get
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1909
|
+
// Realtime
|
|
1910
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1911
|
+
/**
|
|
1912
|
+
* Create a realtime channel.
|
|
1913
|
+
*
|
|
1914
|
+
* @example
|
|
1915
|
+
* const channel = afribase.channel('room:lobby');
|
|
1916
|
+
* channel.on('broadcast', { event: 'message' }, (payload) => console.log(payload)).subscribe();
|
|
1917
|
+
*/
|
|
1918
|
+
channel(name, options) {
|
|
1919
|
+
return this.realtimeClient.channel(name, options);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Remove all realtime channels and disconnect.
|
|
1923
|
+
*/
|
|
1924
|
+
removeAllChannels() {
|
|
1925
|
+
this.realtimeClient.removeAllChannels();
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Get the underlying realtime client.
|
|
1929
|
+
*/
|
|
1930
|
+
get realtime() {
|
|
1931
|
+
return this.realtimeClient;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
var createClient = (baseUrl, anonKey, options) => {
|
|
1935
|
+
return new AfribaseClient(baseUrl, anonKey, options);
|
|
1936
|
+
};
|
|
1937
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1938
|
+
0 && (module.exports = {
|
|
1939
|
+
AfribaseAuthClient,
|
|
1940
|
+
AfribaseClient,
|
|
1941
|
+
AfribaseFunctionsClient,
|
|
1942
|
+
AfribaseQueryBuilder,
|
|
1943
|
+
AfribaseRealtimeClient,
|
|
1944
|
+
AfribaseRpcBuilder,
|
|
1945
|
+
AfribaseStorageClient,
|
|
1946
|
+
RealtimeChannel,
|
|
1947
|
+
StorageFileApi,
|
|
1948
|
+
containedBy,
|
|
1949
|
+
contains,
|
|
1950
|
+
createClient,
|
|
1951
|
+
csv,
|
|
1952
|
+
db,
|
|
1953
|
+
del,
|
|
1954
|
+
eq,
|
|
1955
|
+
explain,
|
|
1956
|
+
geojson,
|
|
1957
|
+
gt,
|
|
1958
|
+
gte,
|
|
1959
|
+
ilike,
|
|
1960
|
+
insert,
|
|
1961
|
+
is,
|
|
1962
|
+
isIn,
|
|
1963
|
+
like,
|
|
1964
|
+
limit,
|
|
1965
|
+
lt,
|
|
1966
|
+
lte,
|
|
1967
|
+
match,
|
|
1968
|
+
maybeSingle,
|
|
1969
|
+
neq,
|
|
1970
|
+
not,
|
|
1971
|
+
or,
|
|
1972
|
+
order,
|
|
1973
|
+
range,
|
|
1974
|
+
select,
|
|
1975
|
+
single,
|
|
1976
|
+
textSearch,
|
|
1977
|
+
update,
|
|
1978
|
+
upsert
|
|
1979
|
+
});
|