@linabase/js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +392 -0
- package/dist/index.js +967 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
type RequestFn$2 = (path: string, options?: RequestInit) => Promise<Response>;
|
|
2
|
+
interface QueryResult<T = any> {
|
|
3
|
+
data: T[] | null;
|
|
4
|
+
error: {
|
|
5
|
+
message: string;
|
|
6
|
+
} | null;
|
|
7
|
+
count: number | null;
|
|
8
|
+
}
|
|
9
|
+
interface SingleQueryResult<T = any> {
|
|
10
|
+
data: T | null;
|
|
11
|
+
error: {
|
|
12
|
+
message: string;
|
|
13
|
+
} | null;
|
|
14
|
+
count: number | null;
|
|
15
|
+
}
|
|
16
|
+
interface CsvQueryResult {
|
|
17
|
+
data: string | null;
|
|
18
|
+
error: {
|
|
19
|
+
message: string;
|
|
20
|
+
} | null;
|
|
21
|
+
count: number | null;
|
|
22
|
+
}
|
|
23
|
+
declare class DatabaseClient {
|
|
24
|
+
private request;
|
|
25
|
+
private table;
|
|
26
|
+
private params;
|
|
27
|
+
private method;
|
|
28
|
+
private body;
|
|
29
|
+
private preferHeaders;
|
|
30
|
+
private _single;
|
|
31
|
+
private _maybeSingle;
|
|
32
|
+
private _throwOnError;
|
|
33
|
+
private _csv;
|
|
34
|
+
private _abortSignal;
|
|
35
|
+
private _schema;
|
|
36
|
+
constructor(request: RequestFn$2, table: string);
|
|
37
|
+
/** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
|
|
38
|
+
select(columns?: string): this;
|
|
39
|
+
insert(data: Record<string, any> | Record<string, any>[]): this;
|
|
40
|
+
/** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
|
|
41
|
+
upsert(data: Record<string, any> | Record<string, any>[], options?: {
|
|
42
|
+
ignoreDuplicates?: boolean;
|
|
43
|
+
}): this;
|
|
44
|
+
update(data: Record<string, any>): this;
|
|
45
|
+
delete(): this;
|
|
46
|
+
eq(column: string, value: any): this;
|
|
47
|
+
neq(column: string, value: any): this;
|
|
48
|
+
gt(column: string, value: any): this;
|
|
49
|
+
gte(column: string, value: any): this;
|
|
50
|
+
lt(column: string, value: any): this;
|
|
51
|
+
lte(column: string, value: any): this;
|
|
52
|
+
like(column: string, pattern: string): this;
|
|
53
|
+
ilike(column: string, pattern: string): this;
|
|
54
|
+
/** Regex match (~) */
|
|
55
|
+
match(column: string, pattern: string): this;
|
|
56
|
+
/** Case-insensitive regex match (~*) */
|
|
57
|
+
imatch(column: string, pattern: string): this;
|
|
58
|
+
in(column: string, values: any[]): this;
|
|
59
|
+
/** Contains (@>) - array or JSONB containment */
|
|
60
|
+
contains(column: string, value: any): this;
|
|
61
|
+
/** Contained by (<@) */
|
|
62
|
+
containedBy(column: string, value: any): this;
|
|
63
|
+
/** Overlap (&&) - ranges or arrays */
|
|
64
|
+
overlaps(column: string, value: any): this;
|
|
65
|
+
is(column: string, value: "null" | "true" | "false"): this;
|
|
66
|
+
/** IS DISTINCT FROM */
|
|
67
|
+
isDistinct(column: string, value: any): this;
|
|
68
|
+
/** Negate a filter: not.eq, not.in, not.is, etc. */
|
|
69
|
+
not(column: string, operator: string, value: any): this;
|
|
70
|
+
/** OR filter: or("age.gt.20,name.eq.John") */
|
|
71
|
+
or(filters: string): this;
|
|
72
|
+
/** to_tsquery */
|
|
73
|
+
textSearch(column: string, query: string, options?: {
|
|
74
|
+
type?: "plain" | "phrase" | "websearch";
|
|
75
|
+
}): this;
|
|
76
|
+
order(column: string, options?: {
|
|
77
|
+
ascending?: boolean;
|
|
78
|
+
nullsFirst?: boolean;
|
|
79
|
+
}): this;
|
|
80
|
+
limit(count: number): this;
|
|
81
|
+
offset(count: number): this;
|
|
82
|
+
/** Request exact, estimated, or planned count via Prefer header */
|
|
83
|
+
count(type?: "exact" | "estimated" | "planned"): this;
|
|
84
|
+
/** Return a single object instead of an array. Errors if 0 or >1 rows. */
|
|
85
|
+
single(): this;
|
|
86
|
+
/** Return a single object or null. Errors only if >1 rows. */
|
|
87
|
+
maybeSingle(): this;
|
|
88
|
+
/** Range-based pagination: range(0, 9) fetches the first 10 rows. */
|
|
89
|
+
range(from: number, to: number): this;
|
|
90
|
+
/** Throw an error instead of returning it in the result object. */
|
|
91
|
+
throwOnError(): this;
|
|
92
|
+
/** Request CSV format. Returns raw CSV text in data instead of parsed objects. */
|
|
93
|
+
csv(): this;
|
|
94
|
+
/** Pass an AbortSignal for request cancellation. */
|
|
95
|
+
abortSignal(signal: AbortSignal): this;
|
|
96
|
+
/** Switch schema. Sets Accept-Profile (GET) or Content-Profile (POST/PATCH/DELETE). */
|
|
97
|
+
schema(schemaName: string): this;
|
|
98
|
+
private reset;
|
|
99
|
+
execute(): Promise<QueryResult | SingleQueryResult | CsvQueryResult>;
|
|
100
|
+
then(resolve: (value: QueryResult | SingleQueryResult | CsvQueryResult) => void, reject?: (reason: any) => void): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
declare class RpcClient {
|
|
103
|
+
private request;
|
|
104
|
+
constructor(request: RequestFn$2);
|
|
105
|
+
/** Call a Postgres function: rpc("my_function", { arg1: "value" }, { count: "exact" }) */
|
|
106
|
+
call(fn: string, args?: Record<string, any>, options?: {
|
|
107
|
+
count?: "exact" | "estimated" | "planned";
|
|
108
|
+
throwOnError?: boolean;
|
|
109
|
+
}): Promise<QueryResult>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type RequestFn$1 = (path: string, options?: RequestInit) => Promise<Response>;
|
|
113
|
+
declare class StorageClient {
|
|
114
|
+
private request;
|
|
115
|
+
private baseUrl;
|
|
116
|
+
constructor(request: RequestFn$1, baseUrl?: string);
|
|
117
|
+
from(bucket: string): BucketClient;
|
|
118
|
+
listBuckets(): Promise<{
|
|
119
|
+
data: any[];
|
|
120
|
+
error: any;
|
|
121
|
+
}>;
|
|
122
|
+
createBucket(name: string, options?: {
|
|
123
|
+
public?: boolean;
|
|
124
|
+
}): Promise<{
|
|
125
|
+
data: any;
|
|
126
|
+
error: any;
|
|
127
|
+
}>;
|
|
128
|
+
}
|
|
129
|
+
declare class BucketClient {
|
|
130
|
+
private request;
|
|
131
|
+
private bucket;
|
|
132
|
+
private baseUrl;
|
|
133
|
+
constructor(request: RequestFn$1, bucket: string, baseUrl?: string);
|
|
134
|
+
upload(path: string, file: Blob | File | ArrayBuffer, options?: {
|
|
135
|
+
contentType?: string;
|
|
136
|
+
}): Promise<{
|
|
137
|
+
data: any;
|
|
138
|
+
error: any;
|
|
139
|
+
}>;
|
|
140
|
+
download(path: string): Promise<{
|
|
141
|
+
data: Blob | null;
|
|
142
|
+
error: any;
|
|
143
|
+
}>;
|
|
144
|
+
list(): Promise<{
|
|
145
|
+
data: any[];
|
|
146
|
+
error: any;
|
|
147
|
+
}>;
|
|
148
|
+
remove(path: string): Promise<{
|
|
149
|
+
error: any;
|
|
150
|
+
}>;
|
|
151
|
+
getPublicUrl(path: string, options?: {
|
|
152
|
+
transform?: {
|
|
153
|
+
width?: number;
|
|
154
|
+
height?: number;
|
|
155
|
+
quality?: number;
|
|
156
|
+
format?: string;
|
|
157
|
+
};
|
|
158
|
+
}): {
|
|
159
|
+
data: {
|
|
160
|
+
publicUrl: string;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
createSignedUrl(path: string, expiresIn: number): Promise<{
|
|
164
|
+
data: {
|
|
165
|
+
signedUrl: string;
|
|
166
|
+
} | null;
|
|
167
|
+
error: any;
|
|
168
|
+
}>;
|
|
169
|
+
createSignedUrls(paths: string[], expiresIn: number): Promise<{
|
|
170
|
+
data: Array<{
|
|
171
|
+
path: string;
|
|
172
|
+
signedUrl: string;
|
|
173
|
+
}> | null;
|
|
174
|
+
error: any;
|
|
175
|
+
}>;
|
|
176
|
+
createSignedUploadUrl(path: string): Promise<{
|
|
177
|
+
data: {
|
|
178
|
+
signedUrl: string;
|
|
179
|
+
token: string;
|
|
180
|
+
path: string;
|
|
181
|
+
} | null;
|
|
182
|
+
error: any;
|
|
183
|
+
}>;
|
|
184
|
+
move(fromPath: string, toPath: string): Promise<{
|
|
185
|
+
error: any;
|
|
186
|
+
}>;
|
|
187
|
+
copy(fromPath: string, toPath: string): Promise<{
|
|
188
|
+
error: any;
|
|
189
|
+
}>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type RequestFn = (path: string, options?: RequestInit) => Promise<Response>;
|
|
193
|
+
type OAuthProvider = "google" | "github" | "apple" | "microsoft" | "linkedin" | "slack" | "gitlab" | "bitbucket" | "discord" | "facebook" | "twitter" | "reddit" | "twitch" | "tiktok" | "spotify" | "telegram" | "roblox";
|
|
194
|
+
interface AuthSession {
|
|
195
|
+
access_token: string;
|
|
196
|
+
refresh_token: string;
|
|
197
|
+
token_type: string;
|
|
198
|
+
expires_in: number;
|
|
199
|
+
expires_at: number;
|
|
200
|
+
user: AuthUser;
|
|
201
|
+
}
|
|
202
|
+
interface AuthUser {
|
|
203
|
+
id: string;
|
|
204
|
+
email: string | null;
|
|
205
|
+
email_confirmed_at: string | null;
|
|
206
|
+
role: string;
|
|
207
|
+
raw_user_meta_data: Record<string, unknown>;
|
|
208
|
+
raw_app_meta_data: Record<string, unknown>;
|
|
209
|
+
created_at: string;
|
|
210
|
+
updated_at: string;
|
|
211
|
+
}
|
|
212
|
+
declare class AuthClient {
|
|
213
|
+
private request;
|
|
214
|
+
private listeners;
|
|
215
|
+
private currentSession;
|
|
216
|
+
/**
|
|
217
|
+
* Callback that the parent LinabaseClient can set to update the Authorization
|
|
218
|
+
* header when the user signs in/out or a token is refreshed.
|
|
219
|
+
*/
|
|
220
|
+
onSessionChange: ((session: AuthSession | null) => void) | null;
|
|
221
|
+
constructor(request: RequestFn);
|
|
222
|
+
private setSession;
|
|
223
|
+
signUp(params: {
|
|
224
|
+
email: string;
|
|
225
|
+
password: string;
|
|
226
|
+
data?: Record<string, unknown>;
|
|
227
|
+
}): Promise<{
|
|
228
|
+
data: AuthSession | null;
|
|
229
|
+
error: any;
|
|
230
|
+
}>;
|
|
231
|
+
signIn(params: {
|
|
232
|
+
email: string;
|
|
233
|
+
password: string;
|
|
234
|
+
}): Promise<{
|
|
235
|
+
data: AuthSession | null;
|
|
236
|
+
error: any;
|
|
237
|
+
}>;
|
|
238
|
+
/** Supabase-compatible alias for signIn */
|
|
239
|
+
signInWithPassword(params: {
|
|
240
|
+
email: string;
|
|
241
|
+
password: string;
|
|
242
|
+
}): Promise<{
|
|
243
|
+
data: AuthSession | null;
|
|
244
|
+
error: any;
|
|
245
|
+
}>;
|
|
246
|
+
signInWithOAuth(params: {
|
|
247
|
+
provider: OAuthProvider;
|
|
248
|
+
redirectTo?: string;
|
|
249
|
+
}): {
|
|
250
|
+
url: string;
|
|
251
|
+
};
|
|
252
|
+
signOut(): Promise<{
|
|
253
|
+
error: any;
|
|
254
|
+
}>;
|
|
255
|
+
getSession(): Promise<{
|
|
256
|
+
data: {
|
|
257
|
+
session: AuthSession | null;
|
|
258
|
+
};
|
|
259
|
+
error: any;
|
|
260
|
+
}>;
|
|
261
|
+
getUser(): Promise<{
|
|
262
|
+
data: {
|
|
263
|
+
user: AuthUser | null;
|
|
264
|
+
};
|
|
265
|
+
error: any;
|
|
266
|
+
}>;
|
|
267
|
+
refreshSession(): Promise<{
|
|
268
|
+
data: AuthSession | null;
|
|
269
|
+
error: any;
|
|
270
|
+
}>;
|
|
271
|
+
resetPasswordForEmail(email: string, _options?: {
|
|
272
|
+
redirectTo?: string;
|
|
273
|
+
}): Promise<{
|
|
274
|
+
error: any;
|
|
275
|
+
}>;
|
|
276
|
+
updatePassword(newPassword: string): Promise<{
|
|
277
|
+
error: any;
|
|
278
|
+
}>;
|
|
279
|
+
updateUser(attributes: {
|
|
280
|
+
email?: string;
|
|
281
|
+
password?: string;
|
|
282
|
+
data?: Record<string, any>;
|
|
283
|
+
}): Promise<{
|
|
284
|
+
data: {
|
|
285
|
+
user: AuthUser | null;
|
|
286
|
+
};
|
|
287
|
+
error: any;
|
|
288
|
+
}>;
|
|
289
|
+
signInWithOtp(params: {
|
|
290
|
+
email: string;
|
|
291
|
+
options?: {
|
|
292
|
+
emailRedirectTo?: string;
|
|
293
|
+
};
|
|
294
|
+
}): Promise<{
|
|
295
|
+
data: any;
|
|
296
|
+
error: any;
|
|
297
|
+
}>;
|
|
298
|
+
verifyOtp(params: {
|
|
299
|
+
email?: string;
|
|
300
|
+
phone?: string;
|
|
301
|
+
token: string;
|
|
302
|
+
type?: "email" | "sms" | "magiclink" | "signup" | "recovery";
|
|
303
|
+
}): Promise<{
|
|
304
|
+
data: any;
|
|
305
|
+
error: any;
|
|
306
|
+
}>;
|
|
307
|
+
get admin(): {
|
|
308
|
+
listUsers(params?: {
|
|
309
|
+
page?: number;
|
|
310
|
+
per_page?: number;
|
|
311
|
+
}): Promise<{
|
|
312
|
+
data: any;
|
|
313
|
+
error: any;
|
|
314
|
+
}>;
|
|
315
|
+
createUser(params: {
|
|
316
|
+
email: string;
|
|
317
|
+
password?: string;
|
|
318
|
+
user_metadata?: Record<string, any>;
|
|
319
|
+
email_confirm?: boolean;
|
|
320
|
+
}): Promise<{
|
|
321
|
+
data: any;
|
|
322
|
+
error: any;
|
|
323
|
+
}>;
|
|
324
|
+
getUserById(id: string): Promise<{
|
|
325
|
+
data: any;
|
|
326
|
+
error: any;
|
|
327
|
+
}>;
|
|
328
|
+
updateUserById(id: string, attributes: Record<string, any>): Promise<{
|
|
329
|
+
data: any;
|
|
330
|
+
error: any;
|
|
331
|
+
}>;
|
|
332
|
+
deleteUser(id: string): Promise<{
|
|
333
|
+
error: any;
|
|
334
|
+
}>;
|
|
335
|
+
};
|
|
336
|
+
get mfa(): {
|
|
337
|
+
enroll(params: {
|
|
338
|
+
factorType: "totp";
|
|
339
|
+
friendlyName?: string;
|
|
340
|
+
}): Promise<{
|
|
341
|
+
data: any;
|
|
342
|
+
error: any;
|
|
343
|
+
}>;
|
|
344
|
+
challenge(params: {
|
|
345
|
+
factorId: string;
|
|
346
|
+
}): Promise<{
|
|
347
|
+
data: any;
|
|
348
|
+
error: any;
|
|
349
|
+
}>;
|
|
350
|
+
verify(params: {
|
|
351
|
+
factorId: string;
|
|
352
|
+
challengeId: string;
|
|
353
|
+
code: string;
|
|
354
|
+
}): Promise<{
|
|
355
|
+
data: any;
|
|
356
|
+
error: any;
|
|
357
|
+
}>;
|
|
358
|
+
unenroll(params: {
|
|
359
|
+
factorId: string;
|
|
360
|
+
}): Promise<{
|
|
361
|
+
error: any;
|
|
362
|
+
}>;
|
|
363
|
+
listFactors(): Promise<{
|
|
364
|
+
data: any;
|
|
365
|
+
error: any;
|
|
366
|
+
}>;
|
|
367
|
+
};
|
|
368
|
+
onAuthStateChange(callback: (event: string, session: AuthSession | null) => void): {
|
|
369
|
+
unsubscribe: () => void;
|
|
370
|
+
};
|
|
371
|
+
private emit;
|
|
372
|
+
private parseAuthResponse;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
interface LinabaseConfig {
|
|
376
|
+
url: string;
|
|
377
|
+
anonKey?: string;
|
|
378
|
+
serviceRoleKey?: string;
|
|
379
|
+
}
|
|
380
|
+
interface LinabaseClient {
|
|
381
|
+
from: (table: string) => DatabaseClient;
|
|
382
|
+
schema: (schemaName: string) => {
|
|
383
|
+
from: (table: string) => DatabaseClient;
|
|
384
|
+
};
|
|
385
|
+
rpc: (fn: string, args?: Record<string, any>) => ReturnType<RpcClient["call"]>;
|
|
386
|
+
storage: StorageClient;
|
|
387
|
+
auth: AuthClient;
|
|
388
|
+
generateTypes: () => Promise<string>;
|
|
389
|
+
}
|
|
390
|
+
declare function createClient(config: LinabaseConfig): LinabaseClient;
|
|
391
|
+
|
|
392
|
+
export { AuthClient, BucketClient, type CsvQueryResult, DatabaseClient, type LinabaseClient, type LinabaseConfig, type OAuthProvider, type QueryResult, RpcClient, type SingleQueryResult, StorageClient, createClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
// src/database.ts
|
|
2
|
+
var DatabaseClient = class {
|
|
3
|
+
request;
|
|
4
|
+
table;
|
|
5
|
+
params;
|
|
6
|
+
method = "GET";
|
|
7
|
+
body = null;
|
|
8
|
+
preferHeaders = [];
|
|
9
|
+
_single = false;
|
|
10
|
+
_maybeSingle = false;
|
|
11
|
+
_throwOnError = false;
|
|
12
|
+
_csv = false;
|
|
13
|
+
_abortSignal;
|
|
14
|
+
_schema;
|
|
15
|
+
constructor(request, table) {
|
|
16
|
+
this.request = request;
|
|
17
|
+
this.table = table;
|
|
18
|
+
this.params = new URLSearchParams();
|
|
19
|
+
}
|
|
20
|
+
// ─── Query Methods ──────────────────────────────────────
|
|
21
|
+
/** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
|
|
22
|
+
select(columns) {
|
|
23
|
+
this.method = "GET";
|
|
24
|
+
if (columns) this.params.set("select", columns);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
insert(data) {
|
|
28
|
+
this.method = "POST";
|
|
29
|
+
this.body = data;
|
|
30
|
+
this.preferHeaders.push("return=representation");
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
/** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
|
|
34
|
+
upsert(data, options) {
|
|
35
|
+
this.method = "POST";
|
|
36
|
+
this.body = data;
|
|
37
|
+
this.preferHeaders.push("return=representation");
|
|
38
|
+
this.preferHeaders.push(
|
|
39
|
+
options?.ignoreDuplicates ? "resolution=ignore-duplicates" : "resolution=merge-duplicates"
|
|
40
|
+
);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
update(data) {
|
|
44
|
+
this.method = "PATCH";
|
|
45
|
+
this.body = data;
|
|
46
|
+
this.preferHeaders.push("return=representation");
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
delete() {
|
|
50
|
+
this.method = "DELETE";
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
// ─── Comparison Filters ─────────────────────────────────
|
|
54
|
+
eq(column, value) {
|
|
55
|
+
this.params.set(column, `eq.${value}`);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
neq(column, value) {
|
|
59
|
+
this.params.set(column, `neq.${value}`);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
gt(column, value) {
|
|
63
|
+
this.params.set(column, `gt.${value}`);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
gte(column, value) {
|
|
67
|
+
this.params.set(column, `gte.${value}`);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
lt(column, value) {
|
|
71
|
+
this.params.set(column, `lt.${value}`);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
lte(column, value) {
|
|
75
|
+
this.params.set(column, `lte.${value}`);
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
// ─── Pattern Matching ───────────────────────────────────
|
|
79
|
+
like(column, pattern) {
|
|
80
|
+
this.params.set(column, `like.${pattern}`);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
ilike(column, pattern) {
|
|
84
|
+
this.params.set(column, `ilike.${pattern}`);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
/** Regex match (~) */
|
|
88
|
+
match(column, pattern) {
|
|
89
|
+
this.params.set(column, `match.${pattern}`);
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
/** Case-insensitive regex match (~*) */
|
|
93
|
+
imatch(column, pattern) {
|
|
94
|
+
this.params.set(column, `imatch.${pattern}`);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
// ─── Array/Set Filters ──────────────────────────────────
|
|
98
|
+
in(column, values) {
|
|
99
|
+
this.params.set(column, `in.(${values.join(",")})`);
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
/** Contains (@>) - array or JSONB containment */
|
|
103
|
+
contains(column, value) {
|
|
104
|
+
this.params.set(column, `cs.${JSON.stringify(value)}`);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
/** Contained by (<@) */
|
|
108
|
+
containedBy(column, value) {
|
|
109
|
+
this.params.set(column, `cd.${JSON.stringify(value)}`);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
/** Overlap (&&) - ranges or arrays */
|
|
113
|
+
overlaps(column, value) {
|
|
114
|
+
this.params.set(column, `ov.${JSON.stringify(value)}`);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
// ─── Null/Boolean ───────────────────────────────────────
|
|
118
|
+
is(column, value) {
|
|
119
|
+
this.params.set(column, `is.${value}`);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
/** IS DISTINCT FROM */
|
|
123
|
+
isDistinct(column, value) {
|
|
124
|
+
this.params.set(column, `isdistinct.${value}`);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
// ─── Negation ───────────────────────────────────────────
|
|
128
|
+
/** Negate a filter: not.eq, not.in, not.is, etc. */
|
|
129
|
+
not(column, operator, value) {
|
|
130
|
+
this.params.set(column, `not.${operator}.${value}`);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
// ─── Logical ────────────────────────────────────────────
|
|
134
|
+
/** OR filter: or("age.gt.20,name.eq.John") */
|
|
135
|
+
or(filters) {
|
|
136
|
+
this.params.set("or", `(${filters})`);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
// ─── Full-Text Search ───────────────────────────────────
|
|
140
|
+
/** to_tsquery */
|
|
141
|
+
textSearch(column, query, options) {
|
|
142
|
+
const op = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : options?.type === "websearch" ? "wfts" : "fts";
|
|
143
|
+
this.params.set(column, `${op}.${query}`);
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
// ─── Ordering & Pagination ──────────────────────────────
|
|
147
|
+
order(column, options) {
|
|
148
|
+
const dir = options?.ascending === false ? "desc" : "asc";
|
|
149
|
+
const nulls = options?.nullsFirst === true ? ".nullsfirst" : options?.nullsFirst === false ? ".nullslast" : "";
|
|
150
|
+
this.params.set("order", `${column}.${dir}${nulls}`);
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
limit(count) {
|
|
154
|
+
this.params.set("limit", String(count));
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
offset(count) {
|
|
158
|
+
this.params.set("offset", String(count));
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
// ─── Count Options ──────────────────────────────────────
|
|
162
|
+
/** Request exact, estimated, or planned count via Prefer header */
|
|
163
|
+
count(type = "exact") {
|
|
164
|
+
this.preferHeaders.push(`count=${type}`);
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
// ─── Convenience Methods ───────────────────────────────
|
|
168
|
+
/** Return a single object instead of an array. Errors if 0 or >1 rows. */
|
|
169
|
+
single() {
|
|
170
|
+
this._single = true;
|
|
171
|
+
this._maybeSingle = false;
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
/** Return a single object or null. Errors only if >1 rows. */
|
|
175
|
+
maybeSingle() {
|
|
176
|
+
this._maybeSingle = true;
|
|
177
|
+
this._single = false;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
/** Range-based pagination: range(0, 9) fetches the first 10 rows. */
|
|
181
|
+
range(from, to) {
|
|
182
|
+
this.params.set("limit", String(to - from + 1));
|
|
183
|
+
this.params.set("offset", String(from));
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
/** Throw an error instead of returning it in the result object. */
|
|
187
|
+
throwOnError() {
|
|
188
|
+
this._throwOnError = true;
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
/** Request CSV format. Returns raw CSV text in data instead of parsed objects. */
|
|
192
|
+
csv() {
|
|
193
|
+
this._csv = true;
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
/** Pass an AbortSignal for request cancellation. */
|
|
197
|
+
abortSignal(signal) {
|
|
198
|
+
this._abortSignal = signal;
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
/** Switch schema. Sets Accept-Profile (GET) or Content-Profile (POST/PATCH/DELETE). */
|
|
202
|
+
schema(schemaName) {
|
|
203
|
+
this._schema = schemaName;
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
// ─── Internal ──────────────────────────────────────────
|
|
207
|
+
reset() {
|
|
208
|
+
this.params = new URLSearchParams();
|
|
209
|
+
this.method = "GET";
|
|
210
|
+
this.body = null;
|
|
211
|
+
this.preferHeaders = [];
|
|
212
|
+
this._single = false;
|
|
213
|
+
this._maybeSingle = false;
|
|
214
|
+
this._throwOnError = false;
|
|
215
|
+
this._csv = false;
|
|
216
|
+
this._abortSignal = void 0;
|
|
217
|
+
this._schema = void 0;
|
|
218
|
+
}
|
|
219
|
+
// ─── Execution ──────────────────────────────────────────
|
|
220
|
+
async execute() {
|
|
221
|
+
const qs = this.params.toString();
|
|
222
|
+
const path = `/rest/v1/${this.table}${qs ? `?${qs}` : ""}`;
|
|
223
|
+
const isSingle = this._single;
|
|
224
|
+
const isMaybeSingle = this._maybeSingle;
|
|
225
|
+
const shouldThrow = this._throwOnError;
|
|
226
|
+
const isCsv = this._csv;
|
|
227
|
+
const abortSignal = this._abortSignal;
|
|
228
|
+
const schema = this._schema;
|
|
229
|
+
const headers = {};
|
|
230
|
+
if (this.preferHeaders.length) {
|
|
231
|
+
headers["Prefer"] = this.preferHeaders.join(", ");
|
|
232
|
+
}
|
|
233
|
+
if (isCsv) {
|
|
234
|
+
headers["Accept"] = "text/csv";
|
|
235
|
+
}
|
|
236
|
+
if (schema) {
|
|
237
|
+
if (this.method === "GET") {
|
|
238
|
+
headers["Accept-Profile"] = schema;
|
|
239
|
+
} else {
|
|
240
|
+
headers["Content-Profile"] = schema;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const makeError = (message) => {
|
|
244
|
+
const result = { data: null, error: { message }, count: null };
|
|
245
|
+
if (shouldThrow) throw new Error(message);
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
248
|
+
try {
|
|
249
|
+
const res = await this.request(path, {
|
|
250
|
+
method: this.method,
|
|
251
|
+
headers,
|
|
252
|
+
body: this.body ? JSON.stringify(this.body) : void 0,
|
|
253
|
+
signal: abortSignal
|
|
254
|
+
});
|
|
255
|
+
const countHeader = res.headers.get("X-Total-Count");
|
|
256
|
+
const totalCount = countHeader ? parseInt(countHeader) : null;
|
|
257
|
+
if (isCsv) {
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
const text = await res.text();
|
|
260
|
+
return makeError(text || res.statusText);
|
|
261
|
+
}
|
|
262
|
+
const csvText = await res.text();
|
|
263
|
+
return { data: csvText, error: null, count: totalCount };
|
|
264
|
+
}
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
if (!res.ok) {
|
|
267
|
+
return makeError(data.error || res.statusText);
|
|
268
|
+
}
|
|
269
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
270
|
+
if (isSingle) {
|
|
271
|
+
if (rows.length === 0) {
|
|
272
|
+
return makeError("No rows found");
|
|
273
|
+
}
|
|
274
|
+
if (rows.length > 1) {
|
|
275
|
+
return makeError("Multiple rows returned for single()");
|
|
276
|
+
}
|
|
277
|
+
const result = {
|
|
278
|
+
data: rows[0],
|
|
279
|
+
error: null,
|
|
280
|
+
count: totalCount
|
|
281
|
+
};
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
if (isMaybeSingle) {
|
|
285
|
+
if (rows.length > 1) {
|
|
286
|
+
return makeError("Multiple rows returned");
|
|
287
|
+
}
|
|
288
|
+
const result = {
|
|
289
|
+
data: rows.length === 1 ? rows[0] : null,
|
|
290
|
+
error: null,
|
|
291
|
+
count: totalCount ?? (rows.length === 0 ? 0 : 1)
|
|
292
|
+
};
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
data: rows,
|
|
297
|
+
error: null,
|
|
298
|
+
count: totalCount
|
|
299
|
+
};
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (shouldThrow) throw err;
|
|
302
|
+
return {
|
|
303
|
+
data: null,
|
|
304
|
+
error: { message: err.message },
|
|
305
|
+
count: null
|
|
306
|
+
};
|
|
307
|
+
} finally {
|
|
308
|
+
this.reset();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
then(resolve, reject) {
|
|
312
|
+
return this.execute().then(resolve, reject);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var RpcClient = class {
|
|
316
|
+
request;
|
|
317
|
+
constructor(request) {
|
|
318
|
+
this.request = request;
|
|
319
|
+
}
|
|
320
|
+
/** Call a Postgres function: rpc("my_function", { arg1: "value" }, { count: "exact" }) */
|
|
321
|
+
async call(fn, args, options) {
|
|
322
|
+
const headers = {};
|
|
323
|
+
const preferParts = [];
|
|
324
|
+
if (options?.count) {
|
|
325
|
+
preferParts.push(`count=${options.count}`);
|
|
326
|
+
}
|
|
327
|
+
if (preferParts.length) {
|
|
328
|
+
headers["Prefer"] = preferParts.join(", ");
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const res = await this.request(`/rest/v1/rpc/${fn}`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers,
|
|
334
|
+
body: JSON.stringify(args || {})
|
|
335
|
+
});
|
|
336
|
+
const countHeader = res.headers.get("X-Total-Count");
|
|
337
|
+
const totalCount = countHeader ? parseInt(countHeader) : null;
|
|
338
|
+
const data = await res.json();
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
const message = data.error || res.statusText;
|
|
341
|
+
if (options?.throwOnError) throw new Error(message);
|
|
342
|
+
return { data: null, error: { message }, count: null };
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
data: Array.isArray(data) ? data : [data],
|
|
346
|
+
error: null,
|
|
347
|
+
count: totalCount
|
|
348
|
+
};
|
|
349
|
+
} catch (err) {
|
|
350
|
+
if (options?.throwOnError) throw err;
|
|
351
|
+
return { data: null, error: { message: err.message }, count: null };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/storage.ts
|
|
357
|
+
var StorageClient = class {
|
|
358
|
+
request;
|
|
359
|
+
baseUrl;
|
|
360
|
+
constructor(request, baseUrl) {
|
|
361
|
+
this.request = request;
|
|
362
|
+
this.baseUrl = baseUrl || "";
|
|
363
|
+
}
|
|
364
|
+
from(bucket) {
|
|
365
|
+
return new BucketClient(this.request, bucket, this.baseUrl);
|
|
366
|
+
}
|
|
367
|
+
async listBuckets() {
|
|
368
|
+
try {
|
|
369
|
+
const res = await this.request("/api/storage/buckets");
|
|
370
|
+
const data = await res.json();
|
|
371
|
+
return { data: data.buckets || [], error: null };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
return { data: [], error: { message: err.message } };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async createBucket(name, options) {
|
|
377
|
+
try {
|
|
378
|
+
const res = await this.request("/api/storage/buckets", {
|
|
379
|
+
method: "POST",
|
|
380
|
+
body: JSON.stringify({ name, isPublic: options?.public })
|
|
381
|
+
});
|
|
382
|
+
const data = await res.json();
|
|
383
|
+
if (!res.ok) return { data: null, error: data };
|
|
384
|
+
return { data: data.bucket, error: null };
|
|
385
|
+
} catch (err) {
|
|
386
|
+
return { data: null, error: { message: err.message } };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var BucketClient = class {
|
|
391
|
+
request;
|
|
392
|
+
bucket;
|
|
393
|
+
baseUrl;
|
|
394
|
+
constructor(request, bucket, baseUrl) {
|
|
395
|
+
this.request = request;
|
|
396
|
+
this.bucket = bucket;
|
|
397
|
+
this.baseUrl = baseUrl || "";
|
|
398
|
+
}
|
|
399
|
+
async upload(path, file, options) {
|
|
400
|
+
try {
|
|
401
|
+
const formData = new FormData();
|
|
402
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: options?.contentType });
|
|
403
|
+
formData.append("file", blob, path);
|
|
404
|
+
const res = await this.request(
|
|
405
|
+
`/storage/${this.bucket}/${path}`,
|
|
406
|
+
{
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: formData,
|
|
409
|
+
headers: {}
|
|
410
|
+
// Let browser set Content-Type for FormData
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
const data = await res.json();
|
|
414
|
+
if (!res.ok) return { data: null, error: data };
|
|
415
|
+
return { data, error: null };
|
|
416
|
+
} catch (err) {
|
|
417
|
+
return { data: null, error: { message: err.message } };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async download(path) {
|
|
421
|
+
try {
|
|
422
|
+
const res = await this.request(
|
|
423
|
+
`/storage/${this.bucket}/${path}`
|
|
424
|
+
);
|
|
425
|
+
if (!res.ok) {
|
|
426
|
+
const err = await res.json().catch(() => ({}));
|
|
427
|
+
return { data: null, error: err };
|
|
428
|
+
}
|
|
429
|
+
const blob = await res.blob();
|
|
430
|
+
return { data: blob, error: null };
|
|
431
|
+
} catch (err) {
|
|
432
|
+
return { data: null, error: { message: err.message } };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async list() {
|
|
436
|
+
try {
|
|
437
|
+
const res = await this.request(
|
|
438
|
+
`/api/storage/objects?bucketName=${this.bucket}`
|
|
439
|
+
);
|
|
440
|
+
const data = await res.json();
|
|
441
|
+
return { data: data.objects || [], error: null };
|
|
442
|
+
} catch (err) {
|
|
443
|
+
return { data: [], error: { message: err.message } };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async remove(path) {
|
|
447
|
+
try {
|
|
448
|
+
const res = await this.request(
|
|
449
|
+
`/storage/${this.bucket}/${path}`,
|
|
450
|
+
{ method: "DELETE" }
|
|
451
|
+
);
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
const data = await res.json().catch(() => ({}));
|
|
454
|
+
return { error: data };
|
|
455
|
+
}
|
|
456
|
+
return { error: null };
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return { error: { message: err.message } };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
getPublicUrl(path, options) {
|
|
462
|
+
let url;
|
|
463
|
+
if (options?.transform) {
|
|
464
|
+
const params = new URLSearchParams();
|
|
465
|
+
if (options.transform.width)
|
|
466
|
+
params.set("width", String(options.transform.width));
|
|
467
|
+
if (options.transform.height)
|
|
468
|
+
params.set("height", String(options.transform.height));
|
|
469
|
+
if (options.transform.quality)
|
|
470
|
+
params.set("quality", String(options.transform.quality));
|
|
471
|
+
if (options.transform.format)
|
|
472
|
+
params.set("format", options.transform.format);
|
|
473
|
+
url = `${this.baseUrl}/storage/transform/${this.bucket}/${path}?${params}`;
|
|
474
|
+
} else {
|
|
475
|
+
url = `${this.baseUrl}/public/${this.bucket}/${path}`;
|
|
476
|
+
}
|
|
477
|
+
return { data: { publicUrl: url } };
|
|
478
|
+
}
|
|
479
|
+
async createSignedUrl(path, expiresIn) {
|
|
480
|
+
try {
|
|
481
|
+
const res = await this.request(
|
|
482
|
+
`/storage/presign/download/${this.bucket}/${path}?expiresIn=${expiresIn}`
|
|
483
|
+
);
|
|
484
|
+
const data = await res.json();
|
|
485
|
+
if (!res.ok) return { data: null, error: data };
|
|
486
|
+
return { data: { signedUrl: data.url || data.signedUrl }, error: null };
|
|
487
|
+
} catch (err) {
|
|
488
|
+
return { data: null, error: { message: err.message } };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async createSignedUrls(paths, expiresIn) {
|
|
492
|
+
const results = await Promise.all(
|
|
493
|
+
paths.map(async (p) => {
|
|
494
|
+
const { data, error } = await this.createSignedUrl(p, expiresIn);
|
|
495
|
+
return { path: p, signedUrl: data?.signedUrl || "", error };
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
const hasError = results.find((r) => r.error);
|
|
499
|
+
if (hasError) return { data: null, error: hasError.error };
|
|
500
|
+
return {
|
|
501
|
+
data: results.map((r) => ({ path: r.path, signedUrl: r.signedUrl })),
|
|
502
|
+
error: null
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async createSignedUploadUrl(path) {
|
|
506
|
+
try {
|
|
507
|
+
const res = await this.request(`/storage/presign/upload`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
body: JSON.stringify({ bucket: this.bucket, path })
|
|
510
|
+
});
|
|
511
|
+
const data = await res.json();
|
|
512
|
+
if (!res.ok) return { data: null, error: data };
|
|
513
|
+
return {
|
|
514
|
+
data: {
|
|
515
|
+
signedUrl: data.url || data.signedUrl,
|
|
516
|
+
token: data.token || "",
|
|
517
|
+
path
|
|
518
|
+
},
|
|
519
|
+
error: null
|
|
520
|
+
};
|
|
521
|
+
} catch (err) {
|
|
522
|
+
return { data: null, error: { message: err.message } };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async move(fromPath, toPath) {
|
|
526
|
+
try {
|
|
527
|
+
const res = await this.request(`/storage/${this.bucket}/move`, {
|
|
528
|
+
method: "POST",
|
|
529
|
+
body: JSON.stringify({ from: fromPath, to: toPath })
|
|
530
|
+
});
|
|
531
|
+
if (!res.ok) {
|
|
532
|
+
const data = await res.json().catch(() => ({}));
|
|
533
|
+
return { error: data };
|
|
534
|
+
}
|
|
535
|
+
return { error: null };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return { error: { message: err.message } };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async copy(fromPath, toPath) {
|
|
541
|
+
try {
|
|
542
|
+
const res = await this.request(`/storage/${this.bucket}/copy`, {
|
|
543
|
+
method: "POST",
|
|
544
|
+
body: JSON.stringify({ from: fromPath, to: toPath })
|
|
545
|
+
});
|
|
546
|
+
if (!res.ok) {
|
|
547
|
+
const data = await res.json().catch(() => ({}));
|
|
548
|
+
return { error: data };
|
|
549
|
+
}
|
|
550
|
+
return { error: null };
|
|
551
|
+
} catch (err) {
|
|
552
|
+
return { error: { message: err.message } };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// src/auth.ts
|
|
558
|
+
var AuthClient = class {
|
|
559
|
+
request;
|
|
560
|
+
listeners = [];
|
|
561
|
+
currentSession = null;
|
|
562
|
+
/**
|
|
563
|
+
* Callback that the parent LinabaseClient can set to update the Authorization
|
|
564
|
+
* header when the user signs in/out or a token is refreshed.
|
|
565
|
+
*/
|
|
566
|
+
onSessionChange = null;
|
|
567
|
+
constructor(request) {
|
|
568
|
+
this.request = request;
|
|
569
|
+
}
|
|
570
|
+
setSession(session) {
|
|
571
|
+
this.currentSession = session;
|
|
572
|
+
if (this.onSessionChange) this.onSessionChange(session);
|
|
573
|
+
}
|
|
574
|
+
// ─── Email/Password ────────────────────────────────────────
|
|
575
|
+
async signUp(params) {
|
|
576
|
+
try {
|
|
577
|
+
const res = await this.request("/auth/v1/signup", {
|
|
578
|
+
method: "POST",
|
|
579
|
+
body: JSON.stringify(params)
|
|
580
|
+
});
|
|
581
|
+
const data = await res.json();
|
|
582
|
+
if (!res.ok) return { data: null, error: data };
|
|
583
|
+
const session = this.parseAuthResponse(data);
|
|
584
|
+
this.setSession(session);
|
|
585
|
+
this.emit("SIGNED_IN", session);
|
|
586
|
+
return { data: session, error: null };
|
|
587
|
+
} catch (err) {
|
|
588
|
+
return { data: null, error: { message: err.message } };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async signIn(params) {
|
|
592
|
+
try {
|
|
593
|
+
const res = await this.request("/auth/v1/token?grant_type=password", {
|
|
594
|
+
method: "POST",
|
|
595
|
+
body: JSON.stringify(params)
|
|
596
|
+
});
|
|
597
|
+
const data = await res.json();
|
|
598
|
+
if (!res.ok) return { data: null, error: data };
|
|
599
|
+
const session = this.parseAuthResponse(data);
|
|
600
|
+
this.setSession(session);
|
|
601
|
+
this.emit("SIGNED_IN", session);
|
|
602
|
+
return { data: session, error: null };
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return { data: null, error: { message: err.message } };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/** Supabase-compatible alias for signIn */
|
|
608
|
+
async signInWithPassword(params) {
|
|
609
|
+
return this.signIn(params);
|
|
610
|
+
}
|
|
611
|
+
// ─── OAuth ─────────────────────────────────────────────────
|
|
612
|
+
signInWithOAuth(params) {
|
|
613
|
+
const queryParams = new URLSearchParams({ provider: params.provider });
|
|
614
|
+
if (params.redirectTo) queryParams.set("redirect_to", params.redirectTo);
|
|
615
|
+
const url = `/auth/v1/authorize?${queryParams}`;
|
|
616
|
+
if (typeof window !== "undefined") {
|
|
617
|
+
window.location.href = url;
|
|
618
|
+
}
|
|
619
|
+
return { url };
|
|
620
|
+
}
|
|
621
|
+
// ─── Session Management ────────────────────────────────────
|
|
622
|
+
async signOut() {
|
|
623
|
+
try {
|
|
624
|
+
await this.request("/auth/v1/logout", { method: "POST" });
|
|
625
|
+
this.setSession(null);
|
|
626
|
+
this.emit("SIGNED_OUT", null);
|
|
627
|
+
return { error: null };
|
|
628
|
+
} catch (err) {
|
|
629
|
+
return { error: { message: err.message } };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async getSession() {
|
|
633
|
+
if (this.currentSession) {
|
|
634
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
635
|
+
if (this.currentSession.expires_at <= now) {
|
|
636
|
+
const refreshed = await this.refreshSession();
|
|
637
|
+
if (refreshed.error) return { data: { session: null }, error: refreshed.error };
|
|
638
|
+
return { data: { session: this.currentSession }, error: null };
|
|
639
|
+
}
|
|
640
|
+
return { data: { session: this.currentSession }, error: null };
|
|
641
|
+
}
|
|
642
|
+
return { data: { session: null }, error: null };
|
|
643
|
+
}
|
|
644
|
+
async getUser() {
|
|
645
|
+
try {
|
|
646
|
+
const res = await this.request("/auth/v1/user");
|
|
647
|
+
const data = await res.json();
|
|
648
|
+
if (!res.ok) return { data: { user: null }, error: data };
|
|
649
|
+
return { data: { user: data }, error: null };
|
|
650
|
+
} catch (err) {
|
|
651
|
+
return { data: { user: null }, error: { message: err.message } };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async refreshSession() {
|
|
655
|
+
if (!this.currentSession?.refresh_token) {
|
|
656
|
+
return { data: null, error: { message: "No refresh token available" } };
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const res = await this.request("/auth/v1/token?grant_type=refresh_token", {
|
|
660
|
+
method: "POST",
|
|
661
|
+
body: JSON.stringify({ refresh_token: this.currentSession.refresh_token })
|
|
662
|
+
});
|
|
663
|
+
const data = await res.json();
|
|
664
|
+
if (!res.ok) {
|
|
665
|
+
this.setSession(null);
|
|
666
|
+
this.emit("SIGNED_OUT", null);
|
|
667
|
+
return { data: null, error: data };
|
|
668
|
+
}
|
|
669
|
+
const session = this.parseAuthResponse(data);
|
|
670
|
+
this.setSession(session);
|
|
671
|
+
this.emit("TOKEN_REFRESHED", session);
|
|
672
|
+
return { data: session, error: null };
|
|
673
|
+
} catch (err) {
|
|
674
|
+
return { data: null, error: { message: err.message } };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// ─── Password Reset ────────────────────────────────────────
|
|
678
|
+
async resetPasswordForEmail(email, _options) {
|
|
679
|
+
try {
|
|
680
|
+
const res = await this.request("/auth/v1/recover", {
|
|
681
|
+
method: "POST",
|
|
682
|
+
body: JSON.stringify({ email })
|
|
683
|
+
});
|
|
684
|
+
if (!res.ok) {
|
|
685
|
+
const data = await res.json();
|
|
686
|
+
return { error: data };
|
|
687
|
+
}
|
|
688
|
+
return { error: null };
|
|
689
|
+
} catch (err) {
|
|
690
|
+
return { error: { message: err.message } };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async updatePassword(newPassword) {
|
|
694
|
+
return this.updateUser({ password: newPassword }).then((r) => ({ error: r.error }));
|
|
695
|
+
}
|
|
696
|
+
// ─── User Update ───────────────────────────────────────────
|
|
697
|
+
async updateUser(attributes) {
|
|
698
|
+
try {
|
|
699
|
+
const res = await this.request("/auth/v1/user", {
|
|
700
|
+
method: "PUT",
|
|
701
|
+
body: JSON.stringify({
|
|
702
|
+
email: attributes.email,
|
|
703
|
+
password: attributes.password,
|
|
704
|
+
user_metadata: attributes.data
|
|
705
|
+
})
|
|
706
|
+
});
|
|
707
|
+
const data = await res.json();
|
|
708
|
+
if (!res.ok) return { data: { user: null }, error: data };
|
|
709
|
+
return { data: { user: data }, error: null };
|
|
710
|
+
} catch (err) {
|
|
711
|
+
return { data: { user: null }, error: { message: err.message } };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// ─── Magic Link / OTP ──────────────────────────────────────
|
|
715
|
+
async signInWithOtp(params) {
|
|
716
|
+
try {
|
|
717
|
+
const res = await this.request("/auth/v1/magiclink", {
|
|
718
|
+
method: "POST",
|
|
719
|
+
body: JSON.stringify({
|
|
720
|
+
email: params.email,
|
|
721
|
+
redirect_to: params.options?.emailRedirectTo
|
|
722
|
+
})
|
|
723
|
+
});
|
|
724
|
+
const data = await res.json();
|
|
725
|
+
if (!res.ok) return { data: null, error: data };
|
|
726
|
+
return { data, error: null };
|
|
727
|
+
} catch (err) {
|
|
728
|
+
return { data: null, error: { message: err.message } };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async verifyOtp(params) {
|
|
732
|
+
try {
|
|
733
|
+
const res = await this.request("/auth/v1/verify", {
|
|
734
|
+
method: "POST",
|
|
735
|
+
body: JSON.stringify({
|
|
736
|
+
type: params.type || "email",
|
|
737
|
+
token: params.token
|
|
738
|
+
})
|
|
739
|
+
});
|
|
740
|
+
const data = await res.json();
|
|
741
|
+
if (!res.ok) return { data: null, error: data };
|
|
742
|
+
this.emit("SIGNED_IN", data);
|
|
743
|
+
return { data, error: null };
|
|
744
|
+
} catch (err) {
|
|
745
|
+
return { data: null, error: { message: err.message } };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// ─── Admin ─────────────────────────────────────────────────
|
|
749
|
+
get admin() {
|
|
750
|
+
const request = this.request;
|
|
751
|
+
return {
|
|
752
|
+
async listUsers(params) {
|
|
753
|
+
try {
|
|
754
|
+
const qs = new URLSearchParams();
|
|
755
|
+
if (params?.page) qs.set("page", String(params.page));
|
|
756
|
+
if (params?.per_page) qs.set("per_page", String(params.per_page));
|
|
757
|
+
const res = await request(`/auth/v1/admin/users?${qs}`);
|
|
758
|
+
const data = await res.json();
|
|
759
|
+
if (!res.ok) return { data: null, error: data };
|
|
760
|
+
return { data, error: null };
|
|
761
|
+
} catch (err) {
|
|
762
|
+
return { data: null, error: { message: err.message } };
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
async createUser(params) {
|
|
766
|
+
try {
|
|
767
|
+
const res = await request("/auth/v1/admin/users", {
|
|
768
|
+
method: "POST",
|
|
769
|
+
body: JSON.stringify(params)
|
|
770
|
+
});
|
|
771
|
+
const data = await res.json();
|
|
772
|
+
if (!res.ok) return { data: null, error: data };
|
|
773
|
+
return { data, error: null };
|
|
774
|
+
} catch (err) {
|
|
775
|
+
return { data: null, error: { message: err.message } };
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
async getUserById(id) {
|
|
779
|
+
try {
|
|
780
|
+
const res = await request(`/auth/v1/admin/users/${id}`);
|
|
781
|
+
const data = await res.json();
|
|
782
|
+
if (!res.ok) return { data: null, error: data };
|
|
783
|
+
return { data, error: null };
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return { data: null, error: { message: err.message } };
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
async updateUserById(id, attributes) {
|
|
789
|
+
try {
|
|
790
|
+
const res = await request(`/auth/v1/admin/users/${id}`, {
|
|
791
|
+
method: "PUT",
|
|
792
|
+
body: JSON.stringify(attributes)
|
|
793
|
+
});
|
|
794
|
+
const data = await res.json();
|
|
795
|
+
if (!res.ok) return { data: null, error: data };
|
|
796
|
+
return { data, error: null };
|
|
797
|
+
} catch (err) {
|
|
798
|
+
return { data: null, error: { message: err.message } };
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
async deleteUser(id) {
|
|
802
|
+
try {
|
|
803
|
+
const res = await request(`/auth/v1/admin/users/${id}`, {
|
|
804
|
+
method: "DELETE"
|
|
805
|
+
});
|
|
806
|
+
if (!res.ok) {
|
|
807
|
+
const data = await res.json().catch(() => ({}));
|
|
808
|
+
return { error: data };
|
|
809
|
+
}
|
|
810
|
+
return { error: null };
|
|
811
|
+
} catch (err) {
|
|
812
|
+
return { error: { message: err.message } };
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
// ─── MFA / TOTP ────────────────────────────────────────────
|
|
818
|
+
get mfa() {
|
|
819
|
+
const request = this.request;
|
|
820
|
+
return {
|
|
821
|
+
async enroll(params) {
|
|
822
|
+
try {
|
|
823
|
+
const res = await request("/auth/v1/factors", {
|
|
824
|
+
method: "POST",
|
|
825
|
+
body: JSON.stringify({ factor_type: params.factorType, friendly_name: params.friendlyName })
|
|
826
|
+
});
|
|
827
|
+
const data = await res.json();
|
|
828
|
+
if (!res.ok) return { data: null, error: data };
|
|
829
|
+
return { data, error: null };
|
|
830
|
+
} catch (err) {
|
|
831
|
+
return { data: null, error: { message: err.message } };
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
async challenge(params) {
|
|
835
|
+
try {
|
|
836
|
+
const res = await request(`/auth/v1/factors/${params.factorId}/challenge`, {
|
|
837
|
+
method: "POST"
|
|
838
|
+
});
|
|
839
|
+
const data = await res.json();
|
|
840
|
+
if (!res.ok) return { data: null, error: data };
|
|
841
|
+
return { data, error: null };
|
|
842
|
+
} catch (err) {
|
|
843
|
+
return { data: null, error: { message: err.message } };
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
async verify(params) {
|
|
847
|
+
try {
|
|
848
|
+
const res = await request(`/auth/v1/factors/${params.factorId}/verify`, {
|
|
849
|
+
method: "POST",
|
|
850
|
+
body: JSON.stringify({ challenge_id: params.challengeId, code: params.code })
|
|
851
|
+
});
|
|
852
|
+
const data = await res.json();
|
|
853
|
+
if (!res.ok) return { data: null, error: data };
|
|
854
|
+
return { data, error: null };
|
|
855
|
+
} catch (err) {
|
|
856
|
+
return { data: null, error: { message: err.message } };
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
async unenroll(params) {
|
|
860
|
+
try {
|
|
861
|
+
const res = await request(`/auth/v1/factors/${params.factorId}`, {
|
|
862
|
+
method: "DELETE"
|
|
863
|
+
});
|
|
864
|
+
if (!res.ok) {
|
|
865
|
+
const data = await res.json().catch(() => ({}));
|
|
866
|
+
return { error: data };
|
|
867
|
+
}
|
|
868
|
+
return { error: null };
|
|
869
|
+
} catch (err) {
|
|
870
|
+
return { error: { message: err.message } };
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
async listFactors() {
|
|
874
|
+
try {
|
|
875
|
+
const res = await request("/auth/v1/factors");
|
|
876
|
+
const data = await res.json();
|
|
877
|
+
if (!res.ok) return { data: null, error: data };
|
|
878
|
+
return { data, error: null };
|
|
879
|
+
} catch (err) {
|
|
880
|
+
return { data: null, error: { message: err.message } };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
// ─── Auth State Listener ───────────────────────────────────
|
|
886
|
+
onAuthStateChange(callback) {
|
|
887
|
+
this.listeners.push(callback);
|
|
888
|
+
return {
|
|
889
|
+
unsubscribe: () => {
|
|
890
|
+
this.listeners = this.listeners.filter((l) => l !== callback);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
emit(event, session) {
|
|
895
|
+
for (const listener of this.listeners) {
|
|
896
|
+
listener(event, session);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
parseAuthResponse(data) {
|
|
900
|
+
return {
|
|
901
|
+
access_token: data.session?.access_token || data.access_token,
|
|
902
|
+
refresh_token: data.session?.refresh_token || data.refresh_token,
|
|
903
|
+
token_type: data.session?.token_type || data.token_type || "bearer",
|
|
904
|
+
expires_in: data.session?.expires_in || data.expires_in || 3600,
|
|
905
|
+
expires_at: data.session?.expires_at || data.expires_at || Math.floor(Date.now() / 1e3) + 3600,
|
|
906
|
+
user: data.user || {}
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// src/client.ts
|
|
912
|
+
function createClient(config) {
|
|
913
|
+
const baseUrl = config.url.replace(/\/$/, "");
|
|
914
|
+
const apiKey = config.serviceRoleKey || config.anonKey || "";
|
|
915
|
+
let accessToken = null;
|
|
916
|
+
async function request(path, options = {}) {
|
|
917
|
+
const url = `${baseUrl}${path}`;
|
|
918
|
+
const mergedHeaders = {
|
|
919
|
+
"Content-Type": "application/json",
|
|
920
|
+
// Always send the API key for project identification
|
|
921
|
+
apikey: apiKey,
|
|
922
|
+
// Use access token if signed in; otherwise use the API key
|
|
923
|
+
Authorization: `Bearer ${accessToken || apiKey}`,
|
|
924
|
+
...options.headers
|
|
925
|
+
};
|
|
926
|
+
if (!options.body) {
|
|
927
|
+
delete mergedHeaders["Content-Type"];
|
|
928
|
+
}
|
|
929
|
+
return fetch(url, {
|
|
930
|
+
...options,
|
|
931
|
+
headers: mergedHeaders
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
const rpcClient = new RpcClient(request);
|
|
935
|
+
const authClient = new AuthClient(request);
|
|
936
|
+
authClient.onSessionChange = (session) => {
|
|
937
|
+
accessToken = session?.access_token || null;
|
|
938
|
+
};
|
|
939
|
+
return {
|
|
940
|
+
from: (table) => new DatabaseClient(request, table),
|
|
941
|
+
schema: (schemaName) => ({
|
|
942
|
+
from: (table) => {
|
|
943
|
+
const client = new DatabaseClient(request, table);
|
|
944
|
+
client._schema = schemaName;
|
|
945
|
+
return client;
|
|
946
|
+
}
|
|
947
|
+
}),
|
|
948
|
+
rpc: (fn, args) => rpcClient.call(fn, args),
|
|
949
|
+
storage: new StorageClient(request, baseUrl),
|
|
950
|
+
auth: authClient,
|
|
951
|
+
generateTypes: async () => {
|
|
952
|
+
const restUrl = baseUrl.replace(/:3100/, ":3107");
|
|
953
|
+
const res = await fetch(`${restUrl}/rest/v1/types`, {
|
|
954
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
955
|
+
});
|
|
956
|
+
return res.text();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
export {
|
|
961
|
+
AuthClient,
|
|
962
|
+
BucketClient,
|
|
963
|
+
DatabaseClient,
|
|
964
|
+
RpcClient,
|
|
965
|
+
StorageClient,
|
|
966
|
+
createClient
|
|
967
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linabase/js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JavaScript/TypeScript client SDK for Linabase (database, storage, auth)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"linabase",
|
|
20
|
+
"supabase",
|
|
21
|
+
"database",
|
|
22
|
+
"storage",
|
|
23
|
+
"auth",
|
|
24
|
+
"baas",
|
|
25
|
+
"sdk",
|
|
26
|
+
"postgres",
|
|
27
|
+
"rls"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/linabase/linabase",
|
|
32
|
+
"directory": "packages/sdk-js"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://linabase.com",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"prepublishOnly": "tsup",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "eslint src/"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@linabase/db": "workspace:*",
|
|
47
|
+
"@linabase/rest-api": "workspace:*",
|
|
48
|
+
"@types/pg": "^8.11.0",
|
|
49
|
+
"pg": "^8.13.0",
|
|
50
|
+
"tsup": "^8.3.0",
|
|
51
|
+
"typescript": "^5.7.0",
|
|
52
|
+
"vitest": "^3.2.1"
|
|
53
|
+
}
|
|
54
|
+
}
|