@linabase/js 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install @linabase/js
14
14
  import { createClient } from "@linabase/js";
15
15
 
16
16
  const linabase = createClient({
17
- url: "https://your-project.linabase.com",
17
+ url: "https://linabase.com",
18
18
  anonKey: "lb_your_anon_key",
19
19
  });
20
20
  ```
@@ -118,7 +118,7 @@ const types = await linabase.generateTypes();
118
118
 
119
119
  ```typescript
120
120
  const admin = createClient({
121
- url: "https://your-project.linabase.com",
121
+ url: "https://linabase.com",
122
122
  serviceRoleKey: "lb_your_service_role_key",
123
123
  });
124
124
 
package/dist/index.cjs CHANGED
@@ -50,7 +50,23 @@ var DatabaseClient = class {
50
50
  this.params = new URLSearchParams();
51
51
  }
52
52
  // ─── Query Methods ──────────────────────────────────────
53
- /** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
53
+ /**
54
+ * Pick which columns to return. Supports nested joins (`"*, comments(*)"`)
55
+ * and column aliases (`"full_name:name"`). When chained after a mutation
56
+ * (`insert` / `update` / `upsert` / `delete`), it switches the request to
57
+ * `Prefer: return=representation` so the modified rows come back.
58
+ *
59
+ * @param columns - Postgres-style column list. Defaults to `"*"`.
60
+ * @param options - `count` returns a row count alongside the data; `head`
61
+ * omits the rows entirely (useful for count-only queries).
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * await linabase.from("posts").select("id, title");
66
+ * await linabase.from("posts").select("*, author:profiles(name)");
67
+ * await linabase.from("posts").select("*", { count: "exact", head: true });
68
+ * ```
69
+ */
54
70
  select(columns, options) {
55
71
  const isMutation = this.method === "POST" || this.method === "PATCH" || this.method === "DELETE";
56
72
  if (!isMutation) {
@@ -62,6 +78,15 @@ var DatabaseClient = class {
62
78
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
63
79
  return this;
64
80
  }
81
+ /**
82
+ * Insert one row or many. Returns the inserted rows by default.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * await linabase.from("posts").insert({ title: "Hi" });
87
+ * await linabase.from("posts").insert([{ title: "A" }, { title: "B" }]);
88
+ * ```
89
+ */
65
90
  insert(data, options) {
66
91
  this.method = "POST";
67
92
  this.body = data;
@@ -83,12 +108,36 @@ var DatabaseClient = class {
83
108
  if (options?.defaultToNull === false) this.preferHeaders.push("missing=default");
84
109
  return this;
85
110
  }
111
+ /**
112
+ * Update rows that match the active filters.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * await linabase
117
+ * .from("posts")
118
+ * .update({ published: true })
119
+ * .eq("id", 42);
120
+ * ```
121
+ */
86
122
  update(data) {
87
123
  this.method = "PATCH";
88
124
  this.body = data;
89
125
  this.preferHeaders.push("return=representation");
90
126
  return this;
91
127
  }
128
+ /**
129
+ * Delete rows that match the active filters. **Always** combine with at
130
+ * least one filter (e.g. `.eq("id", x)`) — calling `.delete()` on a bare
131
+ * builder removes every row in the table.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * await linabase
136
+ * .from("posts")
137
+ * .delete()
138
+ * .eq("id", 42);
139
+ * ```
140
+ */
92
141
  delete(options) {
93
142
  this.method = "DELETE";
94
143
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
@@ -115,36 +164,36 @@ var DatabaseClient = class {
115
164
  }
116
165
  // ─── Comparison Filters ─────────────────────────────────
117
166
  eq(column, value) {
118
- this.params.set(column, `eq.${value}`);
167
+ this.params.append(column, `eq.${value}`);
119
168
  return this;
120
169
  }
121
170
  neq(column, value) {
122
- this.params.set(column, `neq.${value}`);
171
+ this.params.append(column, `neq.${value}`);
123
172
  return this;
124
173
  }
125
174
  gt(column, value) {
126
- this.params.set(column, `gt.${value}`);
175
+ this.params.append(column, `gt.${value}`);
127
176
  return this;
128
177
  }
129
178
  gte(column, value) {
130
- this.params.set(column, `gte.${value}`);
179
+ this.params.append(column, `gte.${value}`);
131
180
  return this;
132
181
  }
133
182
  lt(column, value) {
134
- this.params.set(column, `lt.${value}`);
183
+ this.params.append(column, `lt.${value}`);
135
184
  return this;
136
185
  }
137
186
  lte(column, value) {
138
- this.params.set(column, `lte.${value}`);
187
+ this.params.append(column, `lte.${value}`);
139
188
  return this;
140
189
  }
141
190
  // ─── Pattern Matching ───────────────────────────────────
142
191
  like(column, pattern) {
143
- this.params.set(column, `like.${pattern}`);
192
+ this.params.append(column, `like.${pattern}`);
144
193
  return this;
145
194
  }
146
195
  ilike(column, pattern) {
147
- this.params.set(column, `ilike.${pattern}`);
196
+ this.params.append(column, `ilike.${pattern}`);
148
197
  return this;
149
198
  }
150
199
  /** Regex match (~) when called with (column, pattern). Object-based multi-column filter when called with ({ col: val }). */
@@ -154,76 +203,76 @@ var DatabaseClient = class {
154
203
  this.eq(key, value);
155
204
  }
156
205
  } else if (pattern !== void 0) {
157
- this.params.set(columnOrFilter, `match.${pattern}`);
206
+ this.params.append(columnOrFilter, `match.${pattern}`);
158
207
  }
159
208
  return this;
160
209
  }
161
210
  /** Case-insensitive regex match (~*) */
162
211
  imatch(column, pattern) {
163
- this.params.set(column, `imatch.${pattern}`);
212
+ this.params.append(column, `imatch.${pattern}`);
164
213
  return this;
165
214
  }
166
215
  // ─── Array/Set Filters ──────────────────────────────────
167
216
  in(column, values) {
168
- this.params.set(column, `in.(${values.join(",")})`);
217
+ this.params.append(column, `in.(${values.join(",")})`);
169
218
  return this;
170
219
  }
171
220
  /** Contains (@>) - array or JSONB containment */
172
221
  contains(column, value) {
173
- this.params.set(column, `cs.${JSON.stringify(value)}`);
222
+ this.params.append(column, `cs.${JSON.stringify(value)}`);
174
223
  return this;
175
224
  }
176
225
  /** Contained by (<@) */
177
226
  containedBy(column, value) {
178
- this.params.set(column, `cd.${JSON.stringify(value)}`);
227
+ this.params.append(column, `cd.${JSON.stringify(value)}`);
179
228
  return this;
180
229
  }
181
230
  /** Overlap (&&) - ranges or arrays */
182
231
  overlaps(column, value) {
183
- this.params.set(column, `ov.${JSON.stringify(value)}`);
232
+ this.params.append(column, `ov.${JSON.stringify(value)}`);
184
233
  return this;
185
234
  }
186
235
  // ─── Null/Boolean ───────────────────────────────────────
187
236
  is(column, value) {
188
237
  const v = value === null ? "null" : String(value);
189
- this.params.set(column, `is.${v}`);
238
+ this.params.append(column, `is.${v}`);
190
239
  return this;
191
240
  }
192
241
  /** IS DISTINCT FROM */
193
242
  isDistinct(column, value) {
194
- this.params.set(column, `isdistinct.${value}`);
243
+ this.params.append(column, `isdistinct.${value}`);
195
244
  return this;
196
245
  }
197
246
  // ─── Range Type Filters ──────────────────────────────────
198
247
  /** Strictly left of: [a,b] << [c,d] */
199
248
  rangeLt(column, range) {
200
- this.params.set(column, `sl.${range}`);
249
+ this.params.append(column, `sl.${range}`);
201
250
  return this;
202
251
  }
203
252
  /** Strictly right of: [a,b] >> [c,d] */
204
253
  rangeGt(column, range) {
205
- this.params.set(column, `sr.${range}`);
254
+ this.params.append(column, `sr.${range}`);
206
255
  return this;
207
256
  }
208
257
  /** Does not extend to the left of */
209
258
  rangeGte(column, range) {
210
- this.params.set(column, `nxl.${range}`);
259
+ this.params.append(column, `nxl.${range}`);
211
260
  return this;
212
261
  }
213
262
  /** Does not extend to the right of */
214
263
  rangeLte(column, range) {
215
- this.params.set(column, `nxr.${range}`);
264
+ this.params.append(column, `nxr.${range}`);
216
265
  return this;
217
266
  }
218
267
  /** Range is adjacent to */
219
268
  rangeAdjacent(column, range) {
220
- this.params.set(column, `adj.${range}`);
269
+ this.params.append(column, `adj.${range}`);
221
270
  return this;
222
271
  }
223
272
  // ─── Negation ───────────────────────────────────────────
224
273
  /** Negate a filter: not.eq, not.in, not.is, etc. */
225
274
  not(column, operator, value) {
226
- this.params.set(column, `not.${operator}.${value}`);
275
+ this.params.append(column, `not.${operator}.${value}`);
227
276
  return this;
228
277
  }
229
278
  // ─── Logical ────────────────────────────────────────────
@@ -241,7 +290,7 @@ var DatabaseClient = class {
241
290
  }
242
291
  /** Generic filter: filter("column", "operator", "value") */
243
292
  filter(column, operator, value) {
244
- this.params.set(column, `${operator}.${value}`);
293
+ this.params.append(column, `${operator}.${value}`);
245
294
  return this;
246
295
  }
247
296
  // ─── Full-Text Search ───────────────────────────────────
@@ -249,7 +298,7 @@ var DatabaseClient = class {
249
298
  textSearch(column, query, options) {
250
299
  const op = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : options?.type === "websearch" ? "wfts" : "fts";
251
300
  const config = options?.config ? `(${options.config})` : "";
252
- this.params.set(column, `${op}${config}.${query}`);
301
+ this.params.append(column, `${op}${config}.${query}`);
253
302
  return this;
254
303
  }
255
304
  // ─── Ordering & Pagination ──────────────────────────────
@@ -530,6 +579,24 @@ var BucketClient = class {
530
579
  this.bucket = bucket;
531
580
  this.baseUrl = baseUrl || "";
532
581
  }
582
+ /**
583
+ * Upload a file to the bucket at the given path.
584
+ *
585
+ * @param path - Object key inside the bucket (e.g. `userId/avatar.png`).
586
+ * @param file - File contents as `Blob`, `File`, or `ArrayBuffer`.
587
+ * @param options.contentType - Override the content type. Inferred from
588
+ * `File`/`Blob` when not set.
589
+ * @param options.upsert - If true, replaces an existing object at the same
590
+ * path. Defaults to false (returns an error if the path is taken).
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * const file = e.target.files![0];
595
+ * await linabase.storage
596
+ * .from("avatars")
597
+ * .upload(`${userId}/avatar.png`, file, { upsert: true });
598
+ * ```
599
+ */
533
600
  async upload(path, file, options) {
534
601
  try {
535
602
  const formData = new FormData();
@@ -551,6 +618,18 @@ var BucketClient = class {
551
618
  return { data: null, error: { message: err.message } };
552
619
  }
553
620
  }
621
+ /**
622
+ * Download an object as a `Blob`.
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * const { data } = await linabase.storage.from("docs").download("file.pdf");
627
+ * if (data) {
628
+ * const url = URL.createObjectURL(data);
629
+ * window.open(url);
630
+ * }
631
+ * ```
632
+ */
554
633
  async download(path) {
555
634
  try {
556
635
  const res = await this.request(
@@ -599,6 +678,19 @@ var BucketClient = class {
599
678
  return { data: null, error: { message: err.message } };
600
679
  }
601
680
  }
681
+ /**
682
+ * Build a public URL for an object in a public bucket. No request is made;
683
+ * this just constructs the URL synchronously.
684
+ *
685
+ * For private buckets, use {@link createSignedUrl} instead.
686
+ *
687
+ * @example
688
+ * ```ts
689
+ * const { data: { publicUrl } } = linabase.storage
690
+ * .from("avatars")
691
+ * .getPublicUrl(`${userId}/avatar.png`, { transform: { width: 64, height: 64 } });
692
+ * ```
693
+ */
602
694
  getPublicUrl(path, options) {
603
695
  let url;
604
696
  if (options?.transform) {
@@ -617,6 +709,18 @@ var BucketClient = class {
617
709
  }
618
710
  return { data: { publicUrl: url } };
619
711
  }
712
+ /**
713
+ * Generate a time-limited signed URL for a private object. Valid for
714
+ * `expiresIn` seconds.
715
+ *
716
+ * @example
717
+ * ```ts
718
+ * const { data } = await linabase.storage
719
+ * .from("docs")
720
+ * .createSignedUrl(`${userId}/report.pdf`, 3600);
721
+ * if (data) window.open(data.signedUrl);
722
+ * ```
723
+ */
620
724
  async createSignedUrl(path, expiresIn) {
621
725
  try {
622
726
  const res = await this.request(
@@ -787,6 +891,22 @@ var AuthClient = class {
787
891
  }
788
892
  }
789
893
  // ─── Email/Password ────────────────────────────────────────
894
+ /**
895
+ * Create a new user account with email + password.
896
+ *
897
+ * On success, the user is signed in and the session is persisted. Pass
898
+ * extra metadata via `data`; it is stored on the user record and
899
+ * available later as `user.user_metadata`.
900
+ *
901
+ * @example
902
+ * ```ts
903
+ * const { data, error } = await linabase.auth.signUp({
904
+ * email: "alice@example.com",
905
+ * password: "correct horse battery staple",
906
+ * data: { full_name: "Alice" },
907
+ * });
908
+ * ```
909
+ */
790
910
  async signUp(params) {
791
911
  try {
792
912
  const res = await this.request("/auth/v1/signup", {
@@ -803,6 +923,21 @@ var AuthClient = class {
803
923
  return { data: null, error: { message: err.message } };
804
924
  }
805
925
  }
926
+ /**
927
+ * Sign in an existing user with email + password.
928
+ *
929
+ * Alias of {@link signInWithPassword}. On success, the session is persisted
930
+ * and subsequent SDK calls use the returned access token automatically.
931
+ *
932
+ * @example
933
+ * ```ts
934
+ * const { data, error } = await linabase.auth.signIn({
935
+ * email: "alice@example.com",
936
+ * password: "correct horse battery staple",
937
+ * });
938
+ * if (error) console.error(error.message);
939
+ * ```
940
+ */
806
941
  async signIn(params) {
807
942
  try {
808
943
  const res = await this.request("/auth/v1/token?grant_type=password", {
@@ -844,6 +979,105 @@ var AuthClient = class {
844
979
  return { data: null, error: { message: err.message } };
845
980
  }
846
981
  }
982
+ // ─── Device-bound (Mobile) ──────────────────────────────────
983
+ /**
984
+ * Sign in with a device-bound secret. Intended for mobile clients that
985
+ * generate a random secret on first launch and persist it in the device
986
+ * keychain. No email or password is involved.
987
+ *
988
+ * Pass `create: true` on first launch to register a new anonymous user.
989
+ * Subsequent launches should call without `create` (or with `create: false`)
990
+ * to refuse silent account creation if the keychain entry was lost.
991
+ *
992
+ * Requires "Device-bound sign-ins" to be enabled in the project's Auth
993
+ * Settings.
994
+ *
995
+ * @example
996
+ * ```ts
997
+ * const secret = getOrCreateDeviceSecret();
998
+ * const { data, error } = await linabase.auth.signInWithDevice({
999
+ * deviceSecret: secret,
1000
+ * create: true,
1001
+ * });
1002
+ * ```
1003
+ */
1004
+ async signInWithDevice(params) {
1005
+ try {
1006
+ const res = await this.request("/auth/v1/token?grant_type=device", {
1007
+ method: "POST",
1008
+ body: JSON.stringify({
1009
+ device_secret: params.deviceSecret,
1010
+ create: params.create,
1011
+ data: params.data
1012
+ })
1013
+ });
1014
+ const data = await res.json();
1015
+ if (!res.ok) return { data: null, error: data };
1016
+ const session = this.parseAuthResponse(data);
1017
+ this.setSession(session, true);
1018
+ this.emit("SIGNED_IN", session);
1019
+ return { data: session, error: null };
1020
+ } catch (err) {
1021
+ return { data: null, error: { message: err.message } };
1022
+ }
1023
+ }
1024
+ // ─── Game Center (iOS) ──────────────────────────────────────
1025
+ /**
1026
+ * Sign in with an Apple Game Center identity assertion. The mobile client
1027
+ * obtains the assertion fields via
1028
+ * `GKLocalPlayer.local.fetchItems(forIdentityVerificationSignature:)` (iOS
1029
+ * 13.5+) and forwards them here. The project must have the app's bundle ID
1030
+ * configured in the Game Center allowlist.
1031
+ */
1032
+ async signInWithGameCenter(params) {
1033
+ try {
1034
+ const res = await this.request("/auth/v1/token?grant_type=game_center", {
1035
+ method: "POST",
1036
+ body: JSON.stringify({
1037
+ player_id: params.playerId,
1038
+ bundle_id: params.bundleId,
1039
+ public_key_url: params.publicKeyURL,
1040
+ signature: params.signature,
1041
+ salt: params.salt,
1042
+ timestamp: params.timestamp,
1043
+ display_name: params.displayName
1044
+ })
1045
+ });
1046
+ const data = await res.json();
1047
+ if (!res.ok) return { data: null, error: data };
1048
+ const session = this.parseAuthResponse(data);
1049
+ this.setSession(session, true);
1050
+ this.emit("SIGNED_IN", session);
1051
+ return { data: session, error: null };
1052
+ } catch (err) {
1053
+ return { data: null, error: { message: err.message } };
1054
+ }
1055
+ }
1056
+ // ─── Google Play Games (Android) ────────────────────────────
1057
+ /**
1058
+ * Sign in with a Google Play Games server auth code obtained via
1059
+ * `PlayGamesSignInClient.requestServerSideAccess(serverClientId)`. The
1060
+ * project must have the matching OAuth client configured in Auth Settings.
1061
+ */
1062
+ async signInWithPlayGames(params) {
1063
+ try {
1064
+ const res = await this.request("/auth/v1/token?grant_type=play_games", {
1065
+ method: "POST",
1066
+ body: JSON.stringify({
1067
+ server_auth_code: params.serverAuthCode,
1068
+ redirect_uri: params.redirectUri
1069
+ })
1070
+ });
1071
+ const data = await res.json();
1072
+ if (!res.ok) return { data: null, error: data };
1073
+ const session = this.parseAuthResponse(data);
1074
+ this.setSession(session, true);
1075
+ this.emit("SIGNED_IN", session);
1076
+ return { data: session, error: null };
1077
+ } catch (err) {
1078
+ return { data: null, error: { message: err.message } };
1079
+ }
1080
+ }
847
1081
  // ─── OAuth ─────────────────────────────────────────────────
848
1082
  signInWithOAuth(params) {
849
1083
  const queryParams = new URLSearchParams({ provider: params.provider });
@@ -857,6 +1091,16 @@ var AuthClient = class {
857
1091
  return { url };
858
1092
  }
859
1093
  // ─── Session Management ────────────────────────────────────
1094
+ /**
1095
+ * Sign the current user out, clear the persisted session, and revoke the
1096
+ * refresh token on the server. Fires a `SIGNED_OUT` event to listeners
1097
+ * registered with `onAuthStateChange`.
1098
+ *
1099
+ * @example
1100
+ * ```ts
1101
+ * await linabase.auth.signOut();
1102
+ * ```
1103
+ */
860
1104
  async signOut() {
861
1105
  try {
862
1106
  await this.request("/auth/v1/logout", { method: "POST" });
@@ -867,6 +1111,17 @@ var AuthClient = class {
867
1111
  return { error: { message: err.message } };
868
1112
  }
869
1113
  }
1114
+ /**
1115
+ * Return the current persisted session. Auto-refreshes if the access
1116
+ * token is expired and a refresh token is available. Returns
1117
+ * `{ session: null }` when the user isn't signed in.
1118
+ *
1119
+ * @example
1120
+ * ```ts
1121
+ * const { data: { session } } = await linabase.auth.getSession();
1122
+ * if (session) console.log("Signed in as", session.user.email);
1123
+ * ```
1124
+ */
870
1125
  async getSession() {
871
1126
  if (this.currentSession) {
872
1127
  const now = Math.floor(Date.now() / 1e3);
@@ -1067,6 +1322,41 @@ var AuthClient = class {
1067
1322
  } catch (err) {
1068
1323
  return { error: { message: err.message } };
1069
1324
  }
1325
+ },
1326
+ async listSessions(userId) {
1327
+ try {
1328
+ const res = await request(`/auth/v1/admin/users/${userId}/sessions`);
1329
+ const data = await res.json();
1330
+ if (!res.ok) return { data: null, error: data };
1331
+ return { data: data.sessions, error: null };
1332
+ } catch (err) {
1333
+ return { data: null, error: { message: err.message } };
1334
+ }
1335
+ },
1336
+ async deleteSession(userId, sessionId) {
1337
+ try {
1338
+ const res = await request(`/auth/v1/admin/users/${userId}/sessions/${sessionId}`, {
1339
+ method: "DELETE"
1340
+ });
1341
+ const data = await res.json().catch(() => ({}));
1342
+ if (!res.ok) return { data: null, error: data };
1343
+ return { data, error: null };
1344
+ } catch (err) {
1345
+ return { data: null, error: { message: err.message } };
1346
+ }
1347
+ },
1348
+ async deleteOtherSessions(userId, exceptSessionId) {
1349
+ try {
1350
+ const qs = exceptSessionId ? `?except=${encodeURIComponent(exceptSessionId)}` : "";
1351
+ const res = await request(`/auth/v1/admin/users/${userId}/sessions${qs}`, {
1352
+ method: "DELETE"
1353
+ });
1354
+ const data = await res.json().catch(() => ({}));
1355
+ if (!res.ok) return { data: null, error: data };
1356
+ return { data, error: null };
1357
+ } catch (err) {
1358
+ return { data: null, error: { message: err.message } };
1359
+ }
1070
1360
  }
1071
1361
  };
1072
1362
  }
@@ -1180,6 +1470,19 @@ var FunctionsClient = class {
1180
1470
  constructor(request) {
1181
1471
  this.request = request;
1182
1472
  }
1473
+ /**
1474
+ * Call a function by name.
1475
+ *
1476
+ * @param name - Function slug as registered on the project.
1477
+ * @param options - Body, method, and optional extra headers.
1478
+ * @returns `{ data, error }` — `data` is the parsed JSON response, `error`
1479
+ * is an object with `message` if the call failed (HTTP non-2xx or thrown).
1480
+ *
1481
+ * @example
1482
+ * ```ts
1483
+ * await linabase.functions.invoke("ping");
1484
+ * ```
1485
+ */
1183
1486
  async invoke(name, options) {
1184
1487
  try {
1185
1488
  const method = options?.method || "POST";