@linabase/js 0.2.0 → 0.2.1
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.cjs +1118 -0
- package/dist/index.d.cts +441 -0
- package/package.json +4 -2
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AuthClient: () => AuthClient,
|
|
24
|
+
BucketClient: () => BucketClient,
|
|
25
|
+
DatabaseClient: () => DatabaseClient,
|
|
26
|
+
FunctionsClient: () => FunctionsClient,
|
|
27
|
+
RpcClient: () => RpcClient,
|
|
28
|
+
StorageClient: () => StorageClient,
|
|
29
|
+
createClient: () => createClient
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/database.ts
|
|
34
|
+
var DatabaseClient = class {
|
|
35
|
+
request;
|
|
36
|
+
table;
|
|
37
|
+
params;
|
|
38
|
+
method = "GET";
|
|
39
|
+
body = null;
|
|
40
|
+
preferHeaders = [];
|
|
41
|
+
_single = false;
|
|
42
|
+
_maybeSingle = false;
|
|
43
|
+
_throwOnError = false;
|
|
44
|
+
_csv = false;
|
|
45
|
+
_abortSignal;
|
|
46
|
+
_schema;
|
|
47
|
+
constructor(request, table) {
|
|
48
|
+
this.request = request;
|
|
49
|
+
this.table = table;
|
|
50
|
+
this.params = new URLSearchParams();
|
|
51
|
+
}
|
|
52
|
+
// ─── Query Methods ──────────────────────────────────────
|
|
53
|
+
/** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
|
|
54
|
+
select(columns, options) {
|
|
55
|
+
this.method = "GET";
|
|
56
|
+
if (columns) this.params.set("select", columns);
|
|
57
|
+
if (options?.count) this.preferHeaders.push(`count=${options.count}`);
|
|
58
|
+
if (options?.head) this.method = "GET";
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
insert(data) {
|
|
62
|
+
this.method = "POST";
|
|
63
|
+
this.body = data;
|
|
64
|
+
this.preferHeaders.push("return=representation");
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
/** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
|
|
68
|
+
upsert(data, options) {
|
|
69
|
+
this.method = "POST";
|
|
70
|
+
this.body = data;
|
|
71
|
+
this.preferHeaders.push("return=representation");
|
|
72
|
+
this.preferHeaders.push(
|
|
73
|
+
options?.ignoreDuplicates ? "resolution=ignore-duplicates" : "resolution=merge-duplicates"
|
|
74
|
+
);
|
|
75
|
+
if (options?.onConflict) {
|
|
76
|
+
this.params.set("on_conflict", options.onConflict);
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
update(data) {
|
|
81
|
+
this.method = "PATCH";
|
|
82
|
+
this.body = data;
|
|
83
|
+
this.preferHeaders.push("return=representation");
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
delete() {
|
|
87
|
+
this.method = "DELETE";
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
// ─── Comparison Filters ─────────────────────────────────
|
|
91
|
+
eq(column, value) {
|
|
92
|
+
this.params.set(column, `eq.${value}`);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
neq(column, value) {
|
|
96
|
+
this.params.set(column, `neq.${value}`);
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
gt(column, value) {
|
|
100
|
+
this.params.set(column, `gt.${value}`);
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
gte(column, value) {
|
|
104
|
+
this.params.set(column, `gte.${value}`);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
lt(column, value) {
|
|
108
|
+
this.params.set(column, `lt.${value}`);
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
lte(column, value) {
|
|
112
|
+
this.params.set(column, `lte.${value}`);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
// ─── Pattern Matching ───────────────────────────────────
|
|
116
|
+
like(column, pattern) {
|
|
117
|
+
this.params.set(column, `like.${pattern}`);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
ilike(column, pattern) {
|
|
121
|
+
this.params.set(column, `ilike.${pattern}`);
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
/** Regex match (~) when called with (column, pattern). Object-based multi-column filter when called with ({ col: val }). */
|
|
125
|
+
match(columnOrFilter, pattern) {
|
|
126
|
+
if (typeof columnOrFilter === "object") {
|
|
127
|
+
for (const [key, value] of Object.entries(columnOrFilter)) {
|
|
128
|
+
this.eq(key, value);
|
|
129
|
+
}
|
|
130
|
+
} else if (pattern !== void 0) {
|
|
131
|
+
this.params.set(columnOrFilter, `match.${pattern}`);
|
|
132
|
+
}
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
/** Case-insensitive regex match (~*) */
|
|
136
|
+
imatch(column, pattern) {
|
|
137
|
+
this.params.set(column, `imatch.${pattern}`);
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
// ─── Array/Set Filters ──────────────────────────────────
|
|
141
|
+
in(column, values) {
|
|
142
|
+
this.params.set(column, `in.(${values.join(",")})`);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
/** Contains (@>) - array or JSONB containment */
|
|
146
|
+
contains(column, value) {
|
|
147
|
+
this.params.set(column, `cs.${JSON.stringify(value)}`);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
/** Contained by (<@) */
|
|
151
|
+
containedBy(column, value) {
|
|
152
|
+
this.params.set(column, `cd.${JSON.stringify(value)}`);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
/** Overlap (&&) - ranges or arrays */
|
|
156
|
+
overlaps(column, value) {
|
|
157
|
+
this.params.set(column, `ov.${JSON.stringify(value)}`);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
// ─── Null/Boolean ───────────────────────────────────────
|
|
161
|
+
is(column, value) {
|
|
162
|
+
const v = value === null ? "null" : String(value);
|
|
163
|
+
this.params.set(column, `is.${v}`);
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
/** IS DISTINCT FROM */
|
|
167
|
+
isDistinct(column, value) {
|
|
168
|
+
this.params.set(column, `isdistinct.${value}`);
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
// ─── Negation ───────────────────────────────────────────
|
|
172
|
+
/** Negate a filter: not.eq, not.in, not.is, etc. */
|
|
173
|
+
not(column, operator, value) {
|
|
174
|
+
this.params.set(column, `not.${operator}.${value}`);
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
// ─── Logical ────────────────────────────────────────────
|
|
178
|
+
/** OR filter: or("age.gt.20,name.eq.John") */
|
|
179
|
+
or(filters) {
|
|
180
|
+
this.params.set("or", `(${filters})`);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
// ─── Full-Text Search ───────────────────────────────────
|
|
184
|
+
/** to_tsquery */
|
|
185
|
+
textSearch(column, query, options) {
|
|
186
|
+
const op = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : options?.type === "websearch" ? "wfts" : "fts";
|
|
187
|
+
this.params.set(column, `${op}.${query}`);
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
// ─── Ordering & Pagination ──────────────────────────────
|
|
191
|
+
order(column, options) {
|
|
192
|
+
const dir = options?.ascending === false ? "desc" : "asc";
|
|
193
|
+
const nulls = options?.nullsFirst === true ? ".nullsfirst" : options?.nullsFirst === false ? ".nullslast" : "";
|
|
194
|
+
this.params.set("order", `${column}.${dir}${nulls}`);
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
limit(count) {
|
|
198
|
+
this.params.set("limit", String(count));
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
offset(count) {
|
|
202
|
+
this.params.set("offset", String(count));
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
// ─── Count Options ──────────────────────────────────────
|
|
206
|
+
/** Request exact, estimated, or planned count via Prefer header */
|
|
207
|
+
count(type = "exact") {
|
|
208
|
+
this.preferHeaders.push(`count=${type}`);
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
// ─── Convenience Methods ───────────────────────────────
|
|
212
|
+
/** Return a single object instead of an array. Errors if 0 or >1 rows. */
|
|
213
|
+
single() {
|
|
214
|
+
this._single = true;
|
|
215
|
+
this._maybeSingle = false;
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
/** Return a single object or null. Errors only if >1 rows. */
|
|
219
|
+
maybeSingle() {
|
|
220
|
+
this._maybeSingle = true;
|
|
221
|
+
this._single = false;
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
/** Range-based pagination: range(0, 9) fetches the first 10 rows. */
|
|
225
|
+
range(from, to) {
|
|
226
|
+
this.params.set("limit", String(to - from + 1));
|
|
227
|
+
this.params.set("offset", String(from));
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/** Throw an error instead of returning it in the result object. */
|
|
231
|
+
throwOnError() {
|
|
232
|
+
this._throwOnError = true;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
/** Request CSV format. Returns raw CSV text in data instead of parsed objects. */
|
|
236
|
+
csv() {
|
|
237
|
+
this._csv = true;
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
/** Pass an AbortSignal for request cancellation. */
|
|
241
|
+
abortSignal(signal) {
|
|
242
|
+
this._abortSignal = signal;
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
/** Switch schema. Sets Accept-Profile (GET) or Content-Profile (POST/PATCH/DELETE). */
|
|
246
|
+
schema(schemaName) {
|
|
247
|
+
this._schema = schemaName;
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
// ─── Internal ──────────────────────────────────────────
|
|
251
|
+
reset() {
|
|
252
|
+
this.params = new URLSearchParams();
|
|
253
|
+
this.method = "GET";
|
|
254
|
+
this.body = null;
|
|
255
|
+
this.preferHeaders = [];
|
|
256
|
+
this._single = false;
|
|
257
|
+
this._maybeSingle = false;
|
|
258
|
+
this._throwOnError = false;
|
|
259
|
+
this._csv = false;
|
|
260
|
+
this._abortSignal = void 0;
|
|
261
|
+
this._schema = void 0;
|
|
262
|
+
}
|
|
263
|
+
// ─── Execution ──────────────────────────────────────────
|
|
264
|
+
async execute() {
|
|
265
|
+
const qs = this.params.toString();
|
|
266
|
+
const path = `/rest/v1/${this.table}${qs ? `?${qs}` : ""}`;
|
|
267
|
+
const isSingle = this._single;
|
|
268
|
+
const isMaybeSingle = this._maybeSingle;
|
|
269
|
+
const shouldThrow = this._throwOnError;
|
|
270
|
+
const isCsv = this._csv;
|
|
271
|
+
const abortSignal = this._abortSignal;
|
|
272
|
+
const schema = this._schema;
|
|
273
|
+
const headers = {};
|
|
274
|
+
if (this.preferHeaders.length) {
|
|
275
|
+
headers["Prefer"] = this.preferHeaders.join(", ");
|
|
276
|
+
}
|
|
277
|
+
if (isCsv) {
|
|
278
|
+
headers["Accept"] = "text/csv";
|
|
279
|
+
}
|
|
280
|
+
if (schema) {
|
|
281
|
+
if (this.method === "GET") {
|
|
282
|
+
headers["Accept-Profile"] = schema;
|
|
283
|
+
} else {
|
|
284
|
+
headers["Content-Profile"] = schema;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const makeError = (message) => {
|
|
288
|
+
const result = { data: null, error: { message }, count: null };
|
|
289
|
+
if (shouldThrow) throw new Error(message);
|
|
290
|
+
return result;
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
const res = await this.request(path, {
|
|
294
|
+
method: this.method,
|
|
295
|
+
headers,
|
|
296
|
+
body: this.body ? JSON.stringify(this.body) : void 0,
|
|
297
|
+
signal: abortSignal
|
|
298
|
+
});
|
|
299
|
+
const countHeader = res.headers.get("X-Total-Count");
|
|
300
|
+
const totalCount = countHeader ? parseInt(countHeader) : null;
|
|
301
|
+
if (isCsv) {
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
const text = await res.text();
|
|
304
|
+
return makeError(text || res.statusText);
|
|
305
|
+
}
|
|
306
|
+
const csvText = await res.text();
|
|
307
|
+
return { data: csvText, error: null, count: totalCount };
|
|
308
|
+
}
|
|
309
|
+
const data = await res.json();
|
|
310
|
+
if (!res.ok) {
|
|
311
|
+
return makeError(data.error || res.statusText);
|
|
312
|
+
}
|
|
313
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
314
|
+
if (isSingle) {
|
|
315
|
+
if (rows.length === 0) {
|
|
316
|
+
return makeError("No rows found");
|
|
317
|
+
}
|
|
318
|
+
if (rows.length > 1) {
|
|
319
|
+
return makeError("Multiple rows returned for single()");
|
|
320
|
+
}
|
|
321
|
+
const result = {
|
|
322
|
+
data: rows[0],
|
|
323
|
+
error: null,
|
|
324
|
+
count: totalCount
|
|
325
|
+
};
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
if (isMaybeSingle) {
|
|
329
|
+
if (rows.length > 1) {
|
|
330
|
+
return makeError("Multiple rows returned");
|
|
331
|
+
}
|
|
332
|
+
const result = {
|
|
333
|
+
data: rows.length === 1 ? rows[0] : null,
|
|
334
|
+
error: null,
|
|
335
|
+
count: totalCount ?? (rows.length === 0 ? 0 : 1)
|
|
336
|
+
};
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
data: rows,
|
|
341
|
+
error: null,
|
|
342
|
+
count: totalCount
|
|
343
|
+
};
|
|
344
|
+
} catch (err) {
|
|
345
|
+
if (shouldThrow) throw err;
|
|
346
|
+
return {
|
|
347
|
+
data: null,
|
|
348
|
+
error: { message: err.message },
|
|
349
|
+
count: null
|
|
350
|
+
};
|
|
351
|
+
} finally {
|
|
352
|
+
this.reset();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Makes DatabaseClient thenable (await-able). Resolves to
|
|
357
|
+
* `{ data, error, count }` where data is permissively typed so
|
|
358
|
+
* callers can access properties without generated database types.
|
|
359
|
+
*/
|
|
360
|
+
then(onfulfilled, onrejected) {
|
|
361
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var RpcClient = class {
|
|
365
|
+
request;
|
|
366
|
+
constructor(request) {
|
|
367
|
+
this.request = request;
|
|
368
|
+
}
|
|
369
|
+
/** Call a Postgres function: rpc("my_function", { arg1: "value" }, { count: "exact" }) */
|
|
370
|
+
async call(fn, args, options) {
|
|
371
|
+
const headers = {};
|
|
372
|
+
const preferParts = [];
|
|
373
|
+
if (options?.count) {
|
|
374
|
+
preferParts.push(`count=${options.count}`);
|
|
375
|
+
}
|
|
376
|
+
if (preferParts.length) {
|
|
377
|
+
headers["Prefer"] = preferParts.join(", ");
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const res = await this.request(`/rest/v1/rpc/${fn}`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers,
|
|
383
|
+
body: JSON.stringify(args || {})
|
|
384
|
+
});
|
|
385
|
+
const countHeader = res.headers.get("X-Total-Count");
|
|
386
|
+
const totalCount = countHeader ? parseInt(countHeader) : null;
|
|
387
|
+
const data = await res.json();
|
|
388
|
+
if (!res.ok) {
|
|
389
|
+
const message = data.error || res.statusText;
|
|
390
|
+
if (options?.throwOnError) throw new Error(message);
|
|
391
|
+
return { data: null, error: { message }, count: null };
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
data: Array.isArray(data) ? data : [data],
|
|
395
|
+
error: null,
|
|
396
|
+
count: totalCount
|
|
397
|
+
};
|
|
398
|
+
} catch (err) {
|
|
399
|
+
if (options?.throwOnError) throw err;
|
|
400
|
+
return { data: null, error: { message: err.message }, count: null };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// src/storage.ts
|
|
406
|
+
var StorageClient = class {
|
|
407
|
+
request;
|
|
408
|
+
baseUrl;
|
|
409
|
+
constructor(request, baseUrl) {
|
|
410
|
+
this.request = request;
|
|
411
|
+
this.baseUrl = baseUrl || "";
|
|
412
|
+
}
|
|
413
|
+
from(bucket) {
|
|
414
|
+
return new BucketClient(this.request, bucket, this.baseUrl);
|
|
415
|
+
}
|
|
416
|
+
async listBuckets() {
|
|
417
|
+
try {
|
|
418
|
+
const res = await this.request("/api/storage/buckets");
|
|
419
|
+
const data = await res.json();
|
|
420
|
+
return { data: data.buckets || [], error: null };
|
|
421
|
+
} catch (err) {
|
|
422
|
+
return { data: [], error: { message: err.message } };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async createBucket(name, options) {
|
|
426
|
+
try {
|
|
427
|
+
const res = await this.request("/api/storage/buckets", {
|
|
428
|
+
method: "POST",
|
|
429
|
+
body: JSON.stringify({ name, isPublic: options?.public })
|
|
430
|
+
});
|
|
431
|
+
const data = await res.json();
|
|
432
|
+
if (!res.ok) return { data: null, error: data };
|
|
433
|
+
return { data: data.bucket, error: null };
|
|
434
|
+
} catch (err) {
|
|
435
|
+
return { data: null, error: { message: err.message } };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var BucketClient = class {
|
|
440
|
+
request;
|
|
441
|
+
bucket;
|
|
442
|
+
baseUrl;
|
|
443
|
+
constructor(request, bucket, baseUrl) {
|
|
444
|
+
this.request = request;
|
|
445
|
+
this.bucket = bucket;
|
|
446
|
+
this.baseUrl = baseUrl || "";
|
|
447
|
+
}
|
|
448
|
+
async upload(path, file, options) {
|
|
449
|
+
try {
|
|
450
|
+
const formData = new FormData();
|
|
451
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: options?.contentType });
|
|
452
|
+
formData.append("file", blob, path);
|
|
453
|
+
const res = await this.request(
|
|
454
|
+
`/storage/${this.bucket}/${path}`,
|
|
455
|
+
{
|
|
456
|
+
method: "POST",
|
|
457
|
+
body: formData,
|
|
458
|
+
headers: {}
|
|
459
|
+
// Let browser set Content-Type for FormData
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
const data = await res.json();
|
|
463
|
+
if (!res.ok) return { data: null, error: data };
|
|
464
|
+
return { data, error: null };
|
|
465
|
+
} catch (err) {
|
|
466
|
+
return { data: null, error: { message: err.message } };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async download(path) {
|
|
470
|
+
try {
|
|
471
|
+
const res = await this.request(
|
|
472
|
+
`/storage/${this.bucket}/${path}`
|
|
473
|
+
);
|
|
474
|
+
if (!res.ok) {
|
|
475
|
+
const err = await res.json().catch(() => ({ message: `Request failed (${res.status})` }));
|
|
476
|
+
return { data: null, error: err };
|
|
477
|
+
}
|
|
478
|
+
const blob = await res.blob();
|
|
479
|
+
return { data: blob, error: null };
|
|
480
|
+
} catch (err) {
|
|
481
|
+
return { data: null, error: { message: err.message } };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async list(prefix) {
|
|
485
|
+
try {
|
|
486
|
+
let url = `/api/storage/objects?bucketName=${this.bucket}`;
|
|
487
|
+
if (prefix) url += `&prefix=${encodeURIComponent(prefix)}`;
|
|
488
|
+
const res = await this.request(url);
|
|
489
|
+
const data = await res.json();
|
|
490
|
+
return { data: data.objects || [], error: null };
|
|
491
|
+
} catch (err) {
|
|
492
|
+
return { data: [], error: { message: err.message } };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async remove(pathOrPaths) {
|
|
496
|
+
const paths = Array.isArray(pathOrPaths) ? pathOrPaths : [pathOrPaths];
|
|
497
|
+
try {
|
|
498
|
+
const results = await Promise.all(
|
|
499
|
+
paths.map(async (p) => {
|
|
500
|
+
const res = await this.request(
|
|
501
|
+
`/storage/${this.bucket}/${p}`,
|
|
502
|
+
{ method: "DELETE" }
|
|
503
|
+
);
|
|
504
|
+
if (!res.ok) {
|
|
505
|
+
const data = await res.json().catch(() => ({ message: `Request failed (${res.status})` }));
|
|
506
|
+
return { error: data };
|
|
507
|
+
}
|
|
508
|
+
return { error: null };
|
|
509
|
+
})
|
|
510
|
+
);
|
|
511
|
+
const firstError = results.find((r) => r.error);
|
|
512
|
+
return { data: firstError ? null : paths, error: firstError?.error ?? null };
|
|
513
|
+
} catch (err) {
|
|
514
|
+
return { data: null, error: { message: err.message } };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
getPublicUrl(path, options) {
|
|
518
|
+
let url;
|
|
519
|
+
if (options?.transform) {
|
|
520
|
+
const params = new URLSearchParams();
|
|
521
|
+
if (options.transform.width)
|
|
522
|
+
params.set("width", String(options.transform.width));
|
|
523
|
+
if (options.transform.height)
|
|
524
|
+
params.set("height", String(options.transform.height));
|
|
525
|
+
if (options.transform.quality)
|
|
526
|
+
params.set("quality", String(options.transform.quality));
|
|
527
|
+
if (options.transform.format)
|
|
528
|
+
params.set("format", options.transform.format);
|
|
529
|
+
url = `${this.baseUrl}/storage/transform/${this.bucket}/${path}?${params}`;
|
|
530
|
+
} else {
|
|
531
|
+
url = `${this.baseUrl}/public/${this.bucket}/${path}`;
|
|
532
|
+
}
|
|
533
|
+
return { data: { publicUrl: url } };
|
|
534
|
+
}
|
|
535
|
+
async createSignedUrl(path, expiresIn) {
|
|
536
|
+
try {
|
|
537
|
+
const res = await this.request(
|
|
538
|
+
`/storage/presign/download/${this.bucket}/${path}?expiresIn=${expiresIn}`
|
|
539
|
+
);
|
|
540
|
+
const data = await res.json();
|
|
541
|
+
if (!res.ok) return { data: null, error: data };
|
|
542
|
+
return { data: { signedUrl: data.url || data.signedUrl }, error: null };
|
|
543
|
+
} catch (err) {
|
|
544
|
+
return { data: null, error: { message: err.message } };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async createSignedUrls(paths, expiresIn) {
|
|
548
|
+
const results = await Promise.all(
|
|
549
|
+
paths.map(async (p) => {
|
|
550
|
+
const { data, error } = await this.createSignedUrl(p, expiresIn);
|
|
551
|
+
return { path: p, signedUrl: data?.signedUrl || "", error };
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
const hasError = results.find((r) => r.error);
|
|
555
|
+
if (hasError) return { data: null, error: hasError.error };
|
|
556
|
+
return {
|
|
557
|
+
data: results.map((r) => ({ path: r.path, signedUrl: r.signedUrl })),
|
|
558
|
+
error: null
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
async createSignedUploadUrl(path) {
|
|
562
|
+
try {
|
|
563
|
+
const res = await this.request(`/storage/presign/upload`, {
|
|
564
|
+
method: "POST",
|
|
565
|
+
body: JSON.stringify({ bucket: this.bucket, path })
|
|
566
|
+
});
|
|
567
|
+
const data = await res.json();
|
|
568
|
+
if (!res.ok) return { data: null, error: data };
|
|
569
|
+
return {
|
|
570
|
+
data: {
|
|
571
|
+
signedUrl: data.url || data.signedUrl,
|
|
572
|
+
token: data.token || "",
|
|
573
|
+
path
|
|
574
|
+
},
|
|
575
|
+
error: null
|
|
576
|
+
};
|
|
577
|
+
} catch (err) {
|
|
578
|
+
return { data: null, error: { message: err.message } };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async move(fromPath, toPath) {
|
|
582
|
+
try {
|
|
583
|
+
const res = await this.request(`/storage/${this.bucket}/move`, {
|
|
584
|
+
method: "POST",
|
|
585
|
+
body: JSON.stringify({ from: fromPath, to: toPath })
|
|
586
|
+
});
|
|
587
|
+
if (!res.ok) {
|
|
588
|
+
const data = await res.json().catch(() => ({ message: `Request failed (${res.status})` }));
|
|
589
|
+
return { error: data };
|
|
590
|
+
}
|
|
591
|
+
return { error: null };
|
|
592
|
+
} catch (err) {
|
|
593
|
+
return { error: { message: err.message } };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async copy(fromPath, toPath) {
|
|
597
|
+
try {
|
|
598
|
+
const res = await this.request(`/storage/${this.bucket}/copy`, {
|
|
599
|
+
method: "POST",
|
|
600
|
+
body: JSON.stringify({ from: fromPath, to: toPath })
|
|
601
|
+
});
|
|
602
|
+
if (!res.ok) {
|
|
603
|
+
const data = await res.json().catch(() => ({ message: `Request failed (${res.status})` }));
|
|
604
|
+
return { error: data };
|
|
605
|
+
}
|
|
606
|
+
return { error: null };
|
|
607
|
+
} catch (err) {
|
|
608
|
+
return { error: { message: err.message } };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/auth.ts
|
|
614
|
+
var AuthClient = class {
|
|
615
|
+
request;
|
|
616
|
+
listeners = [];
|
|
617
|
+
currentSession = null;
|
|
618
|
+
/**
|
|
619
|
+
* Callback that the parent LinabaseClient can set to update the Authorization
|
|
620
|
+
* header when the user signs in/out or a token is refreshed.
|
|
621
|
+
*/
|
|
622
|
+
onSessionChange = null;
|
|
623
|
+
constructor(request) {
|
|
624
|
+
this.request = request;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Set (or clear) the current session. Use this to restore a persisted
|
|
628
|
+
* session on app launch (e.g., from AsyncStorage / SecureStore).
|
|
629
|
+
* Emits INITIAL_SESSION to onAuthStateChange listeners.
|
|
630
|
+
*/
|
|
631
|
+
setSession(session) {
|
|
632
|
+
this.currentSession = session;
|
|
633
|
+
if (this.onSessionChange) this.onSessionChange(session);
|
|
634
|
+
this.emit("INITIAL_SESSION", session);
|
|
635
|
+
}
|
|
636
|
+
// ─── Email/Password ────────────────────────────────────────
|
|
637
|
+
async signUp(params) {
|
|
638
|
+
try {
|
|
639
|
+
const res = await this.request("/auth/v1/signup", {
|
|
640
|
+
method: "POST",
|
|
641
|
+
body: JSON.stringify(params)
|
|
642
|
+
});
|
|
643
|
+
const data = await res.json();
|
|
644
|
+
if (!res.ok) return { data: null, error: data };
|
|
645
|
+
const session = this.parseAuthResponse(data);
|
|
646
|
+
this.setSession(session);
|
|
647
|
+
this.emit("SIGNED_IN", session);
|
|
648
|
+
return { data: session, error: null };
|
|
649
|
+
} catch (err) {
|
|
650
|
+
return { data: null, error: { message: err.message } };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async signIn(params) {
|
|
654
|
+
try {
|
|
655
|
+
const res = await this.request("/auth/v1/token?grant_type=password", {
|
|
656
|
+
method: "POST",
|
|
657
|
+
body: JSON.stringify(params)
|
|
658
|
+
});
|
|
659
|
+
const data = await res.json();
|
|
660
|
+
if (!res.ok) return { data: null, error: data };
|
|
661
|
+
const session = this.parseAuthResponse(data);
|
|
662
|
+
this.setSession(session);
|
|
663
|
+
this.emit("SIGNED_IN", session);
|
|
664
|
+
return { data: session, error: null };
|
|
665
|
+
} catch (err) {
|
|
666
|
+
return { data: null, error: { message: err.message } };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** Supabase-compatible alias for signIn */
|
|
670
|
+
async signInWithPassword(params) {
|
|
671
|
+
return this.signIn(params);
|
|
672
|
+
}
|
|
673
|
+
// ─── ID Token (Mobile OAuth) ────────────────────────────────
|
|
674
|
+
async signInWithIdToken(params) {
|
|
675
|
+
try {
|
|
676
|
+
const res = await this.request("/auth/v1/token?grant_type=id_token", {
|
|
677
|
+
method: "POST",
|
|
678
|
+
body: JSON.stringify({
|
|
679
|
+
provider: params.provider,
|
|
680
|
+
id_token: params.token,
|
|
681
|
+
nonce: params.nonce
|
|
682
|
+
})
|
|
683
|
+
});
|
|
684
|
+
const data = await res.json();
|
|
685
|
+
if (!res.ok) return { data: null, error: data };
|
|
686
|
+
const session = this.parseAuthResponse(data);
|
|
687
|
+
this.setSession(session);
|
|
688
|
+
this.emit("SIGNED_IN", session);
|
|
689
|
+
return { data: session, error: null };
|
|
690
|
+
} catch (err) {
|
|
691
|
+
return { data: null, error: { message: err.message } };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// ─── OAuth ─────────────────────────────────────────────────
|
|
695
|
+
signInWithOAuth(params) {
|
|
696
|
+
const queryParams = new URLSearchParams({ provider: params.provider });
|
|
697
|
+
if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
|
|
698
|
+
const url = `/auth/v1/authorize?${queryParams}`;
|
|
699
|
+
if (typeof window !== "undefined") {
|
|
700
|
+
window.location.href = url;
|
|
701
|
+
}
|
|
702
|
+
return { url };
|
|
703
|
+
}
|
|
704
|
+
// ─── Session Management ────────────────────────────────────
|
|
705
|
+
async signOut() {
|
|
706
|
+
try {
|
|
707
|
+
await this.request("/auth/v1/logout", { method: "POST" });
|
|
708
|
+
this.setSession(null);
|
|
709
|
+
this.emit("SIGNED_OUT", null);
|
|
710
|
+
return { error: null };
|
|
711
|
+
} catch (err) {
|
|
712
|
+
return { error: { message: err.message } };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async getSession() {
|
|
716
|
+
if (this.currentSession) {
|
|
717
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
718
|
+
if (this.currentSession.expires_at <= now) {
|
|
719
|
+
const refreshed = await this.refreshSession();
|
|
720
|
+
if (refreshed.error) return { data: { session: null }, error: refreshed.error };
|
|
721
|
+
return { data: { session: this.currentSession }, error: null };
|
|
722
|
+
}
|
|
723
|
+
return { data: { session: this.currentSession }, error: null };
|
|
724
|
+
}
|
|
725
|
+
return { data: { session: null }, error: null };
|
|
726
|
+
}
|
|
727
|
+
async getUser() {
|
|
728
|
+
try {
|
|
729
|
+
const res = await this.request("/auth/v1/user");
|
|
730
|
+
const data = await res.json();
|
|
731
|
+
if (!res.ok) return { data: { user: null }, error: data };
|
|
732
|
+
return { data: { user: data }, error: null };
|
|
733
|
+
} catch (err) {
|
|
734
|
+
return { data: { user: null }, error: { message: err.message } };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async refreshSession() {
|
|
738
|
+
if (!this.currentSession?.refresh_token) {
|
|
739
|
+
return { data: null, error: { message: "No refresh token available" } };
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const res = await this.request("/auth/v1/token?grant_type=refresh_token", {
|
|
743
|
+
method: "POST",
|
|
744
|
+
body: JSON.stringify({ refresh_token: this.currentSession.refresh_token })
|
|
745
|
+
});
|
|
746
|
+
const data = await res.json();
|
|
747
|
+
if (!res.ok) {
|
|
748
|
+
this.setSession(null);
|
|
749
|
+
this.emit("SIGNED_OUT", null);
|
|
750
|
+
return { data: null, error: data };
|
|
751
|
+
}
|
|
752
|
+
const session = this.parseAuthResponse(data);
|
|
753
|
+
this.setSession(session);
|
|
754
|
+
this.emit("TOKEN_REFRESHED", session);
|
|
755
|
+
return { data: session, error: null };
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return { data: null, error: { message: err.message } };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ─── Password Reset ────────────────────────────────────────
|
|
761
|
+
async resetPasswordForEmail(email, _options) {
|
|
762
|
+
try {
|
|
763
|
+
const res = await this.request("/auth/v1/recover", {
|
|
764
|
+
method: "POST",
|
|
765
|
+
body: JSON.stringify({ email })
|
|
766
|
+
});
|
|
767
|
+
if (!res.ok) {
|
|
768
|
+
const data = await res.json();
|
|
769
|
+
return { error: data };
|
|
770
|
+
}
|
|
771
|
+
return { error: null };
|
|
772
|
+
} catch (err) {
|
|
773
|
+
return { error: { message: err.message } };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async updatePassword(newPassword) {
|
|
777
|
+
return this.updateUser({ password: newPassword }).then((r) => ({ error: r.error }));
|
|
778
|
+
}
|
|
779
|
+
// ─── User Update ───────────────────────────────────────────
|
|
780
|
+
async updateUser(attributes) {
|
|
781
|
+
try {
|
|
782
|
+
const res = await this.request("/auth/v1/user", {
|
|
783
|
+
method: "PUT",
|
|
784
|
+
body: JSON.stringify({
|
|
785
|
+
email: attributes.email,
|
|
786
|
+
password: attributes.password,
|
|
787
|
+
user_metadata: attributes.data
|
|
788
|
+
})
|
|
789
|
+
});
|
|
790
|
+
const data = await res.json();
|
|
791
|
+
if (!res.ok) return { data: { user: null }, error: data };
|
|
792
|
+
return { data: { user: data }, error: null };
|
|
793
|
+
} catch (err) {
|
|
794
|
+
return { data: { user: null }, error: { message: err.message } };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// ─── Magic Link / OTP ──────────────────────────────────────
|
|
798
|
+
async signInWithOtp(params) {
|
|
799
|
+
try {
|
|
800
|
+
const res = await this.request("/auth/v1/magiclink", {
|
|
801
|
+
method: "POST",
|
|
802
|
+
body: JSON.stringify({
|
|
803
|
+
email: params.email,
|
|
804
|
+
redirect_to: params.options?.emailRedirectTo
|
|
805
|
+
})
|
|
806
|
+
});
|
|
807
|
+
const data = await res.json();
|
|
808
|
+
if (!res.ok) return { data: null, error: data };
|
|
809
|
+
return { data, error: null };
|
|
810
|
+
} catch (err) {
|
|
811
|
+
return { data: null, error: { message: err.message } };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async verifyOtp(params) {
|
|
815
|
+
try {
|
|
816
|
+
const res = await this.request("/auth/v1/verify", {
|
|
817
|
+
method: "POST",
|
|
818
|
+
body: JSON.stringify({
|
|
819
|
+
type: params.type || "email",
|
|
820
|
+
token: params.token
|
|
821
|
+
})
|
|
822
|
+
});
|
|
823
|
+
const data = await res.json();
|
|
824
|
+
if (!res.ok) return { data: null, error: data };
|
|
825
|
+
this.emit("SIGNED_IN", data);
|
|
826
|
+
return { data, error: null };
|
|
827
|
+
} catch (err) {
|
|
828
|
+
return { data: null, error: { message: err.message } };
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// ─── Admin ─────────────────────────────────────────────────
|
|
832
|
+
get admin() {
|
|
833
|
+
const request = this.request;
|
|
834
|
+
return {
|
|
835
|
+
async listUsers(params) {
|
|
836
|
+
try {
|
|
837
|
+
const qs = new URLSearchParams();
|
|
838
|
+
if (params?.page) qs.set("page", String(params.page));
|
|
839
|
+
if (params?.per_page) qs.set("per_page", String(params.per_page));
|
|
840
|
+
const res = await request(`/auth/v1/admin/users?${qs}`);
|
|
841
|
+
const data = await res.json();
|
|
842
|
+
if (!res.ok) return { data: null, error: data };
|
|
843
|
+
return { data, error: null };
|
|
844
|
+
} catch (err) {
|
|
845
|
+
return { data: null, error: { message: err.message } };
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
async createUser(params) {
|
|
849
|
+
try {
|
|
850
|
+
const res = await request("/auth/v1/admin/users", {
|
|
851
|
+
method: "POST",
|
|
852
|
+
body: JSON.stringify(params)
|
|
853
|
+
});
|
|
854
|
+
const data = await res.json();
|
|
855
|
+
if (!res.ok) return { data: null, error: data };
|
|
856
|
+
return { data, error: null };
|
|
857
|
+
} catch (err) {
|
|
858
|
+
return { data: null, error: { message: err.message } };
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
async getUserById(id) {
|
|
862
|
+
try {
|
|
863
|
+
const res = await request(`/auth/v1/admin/users/${id}`);
|
|
864
|
+
const data = await res.json();
|
|
865
|
+
if (!res.ok) return { data: null, error: data };
|
|
866
|
+
return { data, error: null };
|
|
867
|
+
} catch (err) {
|
|
868
|
+
return { data: null, error: { message: err.message } };
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
async updateUserById(id, attributes) {
|
|
872
|
+
try {
|
|
873
|
+
const res = await request(`/auth/v1/admin/users/${id}`, {
|
|
874
|
+
method: "PUT",
|
|
875
|
+
body: JSON.stringify(attributes)
|
|
876
|
+
});
|
|
877
|
+
const data = await res.json();
|
|
878
|
+
if (!res.ok) return { data: null, error: data };
|
|
879
|
+
return { data, error: null };
|
|
880
|
+
} catch (err) {
|
|
881
|
+
return { data: null, error: { message: err.message } };
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
async deleteUser(id) {
|
|
885
|
+
try {
|
|
886
|
+
const res = await request(`/auth/v1/admin/users/${id}`, {
|
|
887
|
+
method: "DELETE"
|
|
888
|
+
});
|
|
889
|
+
if (!res.ok) {
|
|
890
|
+
const data = await res.json().catch(() => ({ message: `Request failed (${res.status})` }));
|
|
891
|
+
return { error: data };
|
|
892
|
+
}
|
|
893
|
+
return { error: null };
|
|
894
|
+
} catch (err) {
|
|
895
|
+
return { error: { message: err.message } };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
// ─── MFA / TOTP ────────────────────────────────────────────
|
|
901
|
+
get mfa() {
|
|
902
|
+
const request = this.request;
|
|
903
|
+
return {
|
|
904
|
+
async enroll(params) {
|
|
905
|
+
try {
|
|
906
|
+
const res = await request("/auth/v1/factors", {
|
|
907
|
+
method: "POST",
|
|
908
|
+
body: JSON.stringify({ factor_type: params.factorType, friendly_name: params.friendlyName })
|
|
909
|
+
});
|
|
910
|
+
const data = await res.json();
|
|
911
|
+
if (!res.ok) return { data: null, error: data };
|
|
912
|
+
return { data, error: null };
|
|
913
|
+
} catch (err) {
|
|
914
|
+
return { data: null, error: { message: err.message } };
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
async challenge(params) {
|
|
918
|
+
try {
|
|
919
|
+
const res = await request(`/auth/v1/factors/${params.factorId}/challenge`, {
|
|
920
|
+
method: "POST"
|
|
921
|
+
});
|
|
922
|
+
const data = await res.json();
|
|
923
|
+
if (!res.ok) return { data: null, error: data };
|
|
924
|
+
return { data, error: null };
|
|
925
|
+
} catch (err) {
|
|
926
|
+
return { data: null, error: { message: err.message } };
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
async verify(params) {
|
|
930
|
+
try {
|
|
931
|
+
const res = await request(`/auth/v1/factors/${params.factorId}/verify`, {
|
|
932
|
+
method: "POST",
|
|
933
|
+
body: JSON.stringify({ challenge_id: params.challengeId, code: params.code })
|
|
934
|
+
});
|
|
935
|
+
const data = await res.json();
|
|
936
|
+
if (!res.ok) return { data: null, error: data };
|
|
937
|
+
return { data, error: null };
|
|
938
|
+
} catch (err) {
|
|
939
|
+
return { data: null, error: { message: err.message } };
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
async unenroll(params) {
|
|
943
|
+
try {
|
|
944
|
+
const res = await request(`/auth/v1/factors/${params.factorId}`, {
|
|
945
|
+
method: "DELETE"
|
|
946
|
+
});
|
|
947
|
+
if (!res.ok) {
|
|
948
|
+
const data = await res.json().catch(() => ({ message: `Request failed (${res.status})` }));
|
|
949
|
+
return { error: data };
|
|
950
|
+
}
|
|
951
|
+
return { error: null };
|
|
952
|
+
} catch (err) {
|
|
953
|
+
return { error: { message: err.message } };
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
async listFactors() {
|
|
957
|
+
try {
|
|
958
|
+
const res = await request("/auth/v1/factors");
|
|
959
|
+
const data = await res.json();
|
|
960
|
+
if (!res.ok) return { data: null, error: data };
|
|
961
|
+
return { data, error: null };
|
|
962
|
+
} catch (err) {
|
|
963
|
+
return { data: null, error: { message: err.message } };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
// ─── Auth State Listener ───────────────────────────────────
|
|
969
|
+
onAuthStateChange(callback) {
|
|
970
|
+
this.listeners.push(callback);
|
|
971
|
+
return {
|
|
972
|
+
unsubscribe: () => {
|
|
973
|
+
this.listeners = this.listeners.filter((l) => l !== callback);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
emit(event, session) {
|
|
978
|
+
for (const listener of this.listeners) {
|
|
979
|
+
listener(event, session);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
parseAuthResponse(data) {
|
|
983
|
+
const user = data.user || {};
|
|
984
|
+
return {
|
|
985
|
+
access_token: data.session?.access_token || data.access_token,
|
|
986
|
+
refresh_token: data.session?.refresh_token || data.refresh_token,
|
|
987
|
+
token_type: data.session?.token_type || data.token_type || "bearer",
|
|
988
|
+
expires_in: data.session?.expires_in || data.expires_in || 3600,
|
|
989
|
+
expires_at: data.session?.expires_at || data.expires_at || Math.floor(Date.now() / 1e3) + 3600,
|
|
990
|
+
user: {
|
|
991
|
+
...user,
|
|
992
|
+
// Supabase-compatible alias
|
|
993
|
+
user_metadata: user.raw_user_meta_data || user.user_metadata || {}
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// src/functions.ts
|
|
1000
|
+
var FunctionsClient = class {
|
|
1001
|
+
request;
|
|
1002
|
+
constructor(request) {
|
|
1003
|
+
this.request = request;
|
|
1004
|
+
}
|
|
1005
|
+
async invoke(name, options) {
|
|
1006
|
+
try {
|
|
1007
|
+
const method = options?.method || "POST";
|
|
1008
|
+
const init = { method, headers: options?.headers };
|
|
1009
|
+
if (options?.body && method !== "GET") {
|
|
1010
|
+
init.body = JSON.stringify(options.body);
|
|
1011
|
+
}
|
|
1012
|
+
let path = `/functions/v1/${name}`;
|
|
1013
|
+
if (options?.body && method === "GET") {
|
|
1014
|
+
const params = new URLSearchParams();
|
|
1015
|
+
for (const [k, v] of Object.entries(options.body)) {
|
|
1016
|
+
if (v !== void 0 && v !== null) params.set(k, String(v));
|
|
1017
|
+
}
|
|
1018
|
+
path += `?${params}`;
|
|
1019
|
+
}
|
|
1020
|
+
const res = await this.request(path, init);
|
|
1021
|
+
const data = await res.json();
|
|
1022
|
+
if (!res.ok) return { data: null, error: data };
|
|
1023
|
+
return { data, error: null };
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
return { data: null, error: { message: err.message } };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// src/client.ts
|
|
1031
|
+
function createClient(config) {
|
|
1032
|
+
const baseUrl = config.url.replace(/\/$/, "");
|
|
1033
|
+
const apiKey = config.serviceRoleKey || config.anonKey || "";
|
|
1034
|
+
let accessToken = null;
|
|
1035
|
+
async function request(path, options = {}) {
|
|
1036
|
+
const url = `${baseUrl}${path}`;
|
|
1037
|
+
const mergedHeaders = {
|
|
1038
|
+
"Content-Type": "application/json",
|
|
1039
|
+
// Always send the API key for project identification
|
|
1040
|
+
apikey: apiKey,
|
|
1041
|
+
// Use access token if signed in; otherwise use the API key
|
|
1042
|
+
Authorization: `Bearer ${accessToken || apiKey}`,
|
|
1043
|
+
...options.headers
|
|
1044
|
+
};
|
|
1045
|
+
if (!options.body) {
|
|
1046
|
+
delete mergedHeaders["Content-Type"];
|
|
1047
|
+
}
|
|
1048
|
+
return fetch(url, {
|
|
1049
|
+
...options,
|
|
1050
|
+
headers: mergedHeaders
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const authClient = new AuthClient(request);
|
|
1054
|
+
authClient.onSessionChange = (session) => {
|
|
1055
|
+
accessToken = session?.access_token || null;
|
|
1056
|
+
};
|
|
1057
|
+
function buildClient(reqFn, branchSlug) {
|
|
1058
|
+
const rc = new RpcClient(reqFn);
|
|
1059
|
+
const ac = new AuthClient(reqFn);
|
|
1060
|
+
ac.onSessionChange = (session) => {
|
|
1061
|
+
accessToken = session?.access_token || null;
|
|
1062
|
+
};
|
|
1063
|
+
return {
|
|
1064
|
+
from: (table) => new DatabaseClient(reqFn, table),
|
|
1065
|
+
schema: (schemaName) => ({
|
|
1066
|
+
from: (table) => {
|
|
1067
|
+
const client = new DatabaseClient(reqFn, table);
|
|
1068
|
+
client._schema = schemaName;
|
|
1069
|
+
return client;
|
|
1070
|
+
}
|
|
1071
|
+
}),
|
|
1072
|
+
rpc: (fn, args) => rc.call(fn, args),
|
|
1073
|
+
storage: new StorageClient(reqFn, baseUrl),
|
|
1074
|
+
auth: branchSlug ? authClient : ac,
|
|
1075
|
+
functions: new FunctionsClient(reqFn),
|
|
1076
|
+
/** Realtime channel (stub; not yet supported). Returns a chainable no-op. */
|
|
1077
|
+
channel: (_name) => {
|
|
1078
|
+
const noop = { on: () => noop, subscribe: () => noop, unsubscribe: () => {
|
|
1079
|
+
} };
|
|
1080
|
+
return noop;
|
|
1081
|
+
},
|
|
1082
|
+
/** Remove a realtime channel (stub; not yet supported). */
|
|
1083
|
+
removeChannel: (_channel) => {
|
|
1084
|
+
},
|
|
1085
|
+
generateTypes: async () => {
|
|
1086
|
+
const restUrl = baseUrl.replace(/:3100/, ":3107");
|
|
1087
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
1088
|
+
if (branchSlug) headers["X-Branch"] = branchSlug;
|
|
1089
|
+
const res = await fetch(`${restUrl}/rest/v1/types`, { headers });
|
|
1090
|
+
return res.text();
|
|
1091
|
+
},
|
|
1092
|
+
branch: (slug) => {
|
|
1093
|
+
if (branchSlug) throw new Error("Cannot nest branch() calls");
|
|
1094
|
+
function branchRequest(path, options = {}) {
|
|
1095
|
+
return reqFn(path, {
|
|
1096
|
+
...options,
|
|
1097
|
+
headers: {
|
|
1098
|
+
...options.headers,
|
|
1099
|
+
"X-Branch": slug
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
return buildClient(branchRequest, slug);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
return buildClient(request);
|
|
1108
|
+
}
|
|
1109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1110
|
+
0 && (module.exports = {
|
|
1111
|
+
AuthClient,
|
|
1112
|
+
BucketClient,
|
|
1113
|
+
DatabaseClient,
|
|
1114
|
+
FunctionsClient,
|
|
1115
|
+
RpcClient,
|
|
1116
|
+
StorageClient,
|
|
1117
|
+
createClient
|
|
1118
|
+
});
|