@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/dist/index.js CHANGED
@@ -18,7 +18,23 @@ var DatabaseClient = class {
18
18
  this.params = new URLSearchParams();
19
19
  }
20
20
  // ─── Query Methods ──────────────────────────────────────
21
- /** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
21
+ /**
22
+ * Pick which columns to return. Supports nested joins (`"*, comments(*)"`)
23
+ * and column aliases (`"full_name:name"`). When chained after a mutation
24
+ * (`insert` / `update` / `upsert` / `delete`), it switches the request to
25
+ * `Prefer: return=representation` so the modified rows come back.
26
+ *
27
+ * @param columns - Postgres-style column list. Defaults to `"*"`.
28
+ * @param options - `count` returns a row count alongside the data; `head`
29
+ * omits the rows entirely (useful for count-only queries).
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * await linabase.from("posts").select("id, title");
34
+ * await linabase.from("posts").select("*, author:profiles(name)");
35
+ * await linabase.from("posts").select("*", { count: "exact", head: true });
36
+ * ```
37
+ */
22
38
  select(columns, options) {
23
39
  const isMutation = this.method === "POST" || this.method === "PATCH" || this.method === "DELETE";
24
40
  if (!isMutation) {
@@ -30,6 +46,15 @@ var DatabaseClient = class {
30
46
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
31
47
  return this;
32
48
  }
49
+ /**
50
+ * Insert one row or many. Returns the inserted rows by default.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * await linabase.from("posts").insert({ title: "Hi" });
55
+ * await linabase.from("posts").insert([{ title: "A" }, { title: "B" }]);
56
+ * ```
57
+ */
33
58
  insert(data, options) {
34
59
  this.method = "POST";
35
60
  this.body = data;
@@ -51,12 +76,36 @@ var DatabaseClient = class {
51
76
  if (options?.defaultToNull === false) this.preferHeaders.push("missing=default");
52
77
  return this;
53
78
  }
79
+ /**
80
+ * Update rows that match the active filters.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * await linabase
85
+ * .from("posts")
86
+ * .update({ published: true })
87
+ * .eq("id", 42);
88
+ * ```
89
+ */
54
90
  update(data) {
55
91
  this.method = "PATCH";
56
92
  this.body = data;
57
93
  this.preferHeaders.push("return=representation");
58
94
  return this;
59
95
  }
96
+ /**
97
+ * Delete rows that match the active filters. **Always** combine with at
98
+ * least one filter (e.g. `.eq("id", x)`) — calling `.delete()` on a bare
99
+ * builder removes every row in the table.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * await linabase
104
+ * .from("posts")
105
+ * .delete()
106
+ * .eq("id", 42);
107
+ * ```
108
+ */
60
109
  delete(options) {
61
110
  this.method = "DELETE";
62
111
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
@@ -83,36 +132,36 @@ var DatabaseClient = class {
83
132
  }
84
133
  // ─── Comparison Filters ─────────────────────────────────
85
134
  eq(column, value) {
86
- this.params.set(column, `eq.${value}`);
135
+ this.params.append(column, `eq.${value}`);
87
136
  return this;
88
137
  }
89
138
  neq(column, value) {
90
- this.params.set(column, `neq.${value}`);
139
+ this.params.append(column, `neq.${value}`);
91
140
  return this;
92
141
  }
93
142
  gt(column, value) {
94
- this.params.set(column, `gt.${value}`);
143
+ this.params.append(column, `gt.${value}`);
95
144
  return this;
96
145
  }
97
146
  gte(column, value) {
98
- this.params.set(column, `gte.${value}`);
147
+ this.params.append(column, `gte.${value}`);
99
148
  return this;
100
149
  }
101
150
  lt(column, value) {
102
- this.params.set(column, `lt.${value}`);
151
+ this.params.append(column, `lt.${value}`);
103
152
  return this;
104
153
  }
105
154
  lte(column, value) {
106
- this.params.set(column, `lte.${value}`);
155
+ this.params.append(column, `lte.${value}`);
107
156
  return this;
108
157
  }
109
158
  // ─── Pattern Matching ───────────────────────────────────
110
159
  like(column, pattern) {
111
- this.params.set(column, `like.${pattern}`);
160
+ this.params.append(column, `like.${pattern}`);
112
161
  return this;
113
162
  }
114
163
  ilike(column, pattern) {
115
- this.params.set(column, `ilike.${pattern}`);
164
+ this.params.append(column, `ilike.${pattern}`);
116
165
  return this;
117
166
  }
118
167
  /** Regex match (~) when called with (column, pattern). Object-based multi-column filter when called with ({ col: val }). */
@@ -122,76 +171,76 @@ var DatabaseClient = class {
122
171
  this.eq(key, value);
123
172
  }
124
173
  } else if (pattern !== void 0) {
125
- this.params.set(columnOrFilter, `match.${pattern}`);
174
+ this.params.append(columnOrFilter, `match.${pattern}`);
126
175
  }
127
176
  return this;
128
177
  }
129
178
  /** Case-insensitive regex match (~*) */
130
179
  imatch(column, pattern) {
131
- this.params.set(column, `imatch.${pattern}`);
180
+ this.params.append(column, `imatch.${pattern}`);
132
181
  return this;
133
182
  }
134
183
  // ─── Array/Set Filters ──────────────────────────────────
135
184
  in(column, values) {
136
- this.params.set(column, `in.(${values.join(",")})`);
185
+ this.params.append(column, `in.(${values.join(",")})`);
137
186
  return this;
138
187
  }
139
188
  /** Contains (@>) - array or JSONB containment */
140
189
  contains(column, value) {
141
- this.params.set(column, `cs.${JSON.stringify(value)}`);
190
+ this.params.append(column, `cs.${JSON.stringify(value)}`);
142
191
  return this;
143
192
  }
144
193
  /** Contained by (<@) */
145
194
  containedBy(column, value) {
146
- this.params.set(column, `cd.${JSON.stringify(value)}`);
195
+ this.params.append(column, `cd.${JSON.stringify(value)}`);
147
196
  return this;
148
197
  }
149
198
  /** Overlap (&&) - ranges or arrays */
150
199
  overlaps(column, value) {
151
- this.params.set(column, `ov.${JSON.stringify(value)}`);
200
+ this.params.append(column, `ov.${JSON.stringify(value)}`);
152
201
  return this;
153
202
  }
154
203
  // ─── Null/Boolean ───────────────────────────────────────
155
204
  is(column, value) {
156
205
  const v = value === null ? "null" : String(value);
157
- this.params.set(column, `is.${v}`);
206
+ this.params.append(column, `is.${v}`);
158
207
  return this;
159
208
  }
160
209
  /** IS DISTINCT FROM */
161
210
  isDistinct(column, value) {
162
- this.params.set(column, `isdistinct.${value}`);
211
+ this.params.append(column, `isdistinct.${value}`);
163
212
  return this;
164
213
  }
165
214
  // ─── Range Type Filters ──────────────────────────────────
166
215
  /** Strictly left of: [a,b] << [c,d] */
167
216
  rangeLt(column, range) {
168
- this.params.set(column, `sl.${range}`);
217
+ this.params.append(column, `sl.${range}`);
169
218
  return this;
170
219
  }
171
220
  /** Strictly right of: [a,b] >> [c,d] */
172
221
  rangeGt(column, range) {
173
- this.params.set(column, `sr.${range}`);
222
+ this.params.append(column, `sr.${range}`);
174
223
  return this;
175
224
  }
176
225
  /** Does not extend to the left of */
177
226
  rangeGte(column, range) {
178
- this.params.set(column, `nxl.${range}`);
227
+ this.params.append(column, `nxl.${range}`);
179
228
  return this;
180
229
  }
181
230
  /** Does not extend to the right of */
182
231
  rangeLte(column, range) {
183
- this.params.set(column, `nxr.${range}`);
232
+ this.params.append(column, `nxr.${range}`);
184
233
  return this;
185
234
  }
186
235
  /** Range is adjacent to */
187
236
  rangeAdjacent(column, range) {
188
- this.params.set(column, `adj.${range}`);
237
+ this.params.append(column, `adj.${range}`);
189
238
  return this;
190
239
  }
191
240
  // ─── Negation ───────────────────────────────────────────
192
241
  /** Negate a filter: not.eq, not.in, not.is, etc. */
193
242
  not(column, operator, value) {
194
- this.params.set(column, `not.${operator}.${value}`);
243
+ this.params.append(column, `not.${operator}.${value}`);
195
244
  return this;
196
245
  }
197
246
  // ─── Logical ────────────────────────────────────────────
@@ -209,7 +258,7 @@ var DatabaseClient = class {
209
258
  }
210
259
  /** Generic filter: filter("column", "operator", "value") */
211
260
  filter(column, operator, value) {
212
- this.params.set(column, `${operator}.${value}`);
261
+ this.params.append(column, `${operator}.${value}`);
213
262
  return this;
214
263
  }
215
264
  // ─── Full-Text Search ───────────────────────────────────
@@ -217,7 +266,7 @@ var DatabaseClient = class {
217
266
  textSearch(column, query, options) {
218
267
  const op = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : options?.type === "websearch" ? "wfts" : "fts";
219
268
  const config = options?.config ? `(${options.config})` : "";
220
- this.params.set(column, `${op}${config}.${query}`);
269
+ this.params.append(column, `${op}${config}.${query}`);
221
270
  return this;
222
271
  }
223
272
  // ─── Ordering & Pagination ──────────────────────────────
@@ -498,6 +547,24 @@ var BucketClient = class {
498
547
  this.bucket = bucket;
499
548
  this.baseUrl = baseUrl || "";
500
549
  }
550
+ /**
551
+ * Upload a file to the bucket at the given path.
552
+ *
553
+ * @param path - Object key inside the bucket (e.g. `userId/avatar.png`).
554
+ * @param file - File contents as `Blob`, `File`, or `ArrayBuffer`.
555
+ * @param options.contentType - Override the content type. Inferred from
556
+ * `File`/`Blob` when not set.
557
+ * @param options.upsert - If true, replaces an existing object at the same
558
+ * path. Defaults to false (returns an error if the path is taken).
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const file = e.target.files![0];
563
+ * await linabase.storage
564
+ * .from("avatars")
565
+ * .upload(`${userId}/avatar.png`, file, { upsert: true });
566
+ * ```
567
+ */
501
568
  async upload(path, file, options) {
502
569
  try {
503
570
  const formData = new FormData();
@@ -519,6 +586,18 @@ var BucketClient = class {
519
586
  return { data: null, error: { message: err.message } };
520
587
  }
521
588
  }
589
+ /**
590
+ * Download an object as a `Blob`.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * const { data } = await linabase.storage.from("docs").download("file.pdf");
595
+ * if (data) {
596
+ * const url = URL.createObjectURL(data);
597
+ * window.open(url);
598
+ * }
599
+ * ```
600
+ */
522
601
  async download(path) {
523
602
  try {
524
603
  const res = await this.request(
@@ -567,6 +646,19 @@ var BucketClient = class {
567
646
  return { data: null, error: { message: err.message } };
568
647
  }
569
648
  }
649
+ /**
650
+ * Build a public URL for an object in a public bucket. No request is made;
651
+ * this just constructs the URL synchronously.
652
+ *
653
+ * For private buckets, use {@link createSignedUrl} instead.
654
+ *
655
+ * @example
656
+ * ```ts
657
+ * const { data: { publicUrl } } = linabase.storage
658
+ * .from("avatars")
659
+ * .getPublicUrl(`${userId}/avatar.png`, { transform: { width: 64, height: 64 } });
660
+ * ```
661
+ */
570
662
  getPublicUrl(path, options) {
571
663
  let url;
572
664
  if (options?.transform) {
@@ -585,6 +677,18 @@ var BucketClient = class {
585
677
  }
586
678
  return { data: { publicUrl: url } };
587
679
  }
680
+ /**
681
+ * Generate a time-limited signed URL for a private object. Valid for
682
+ * `expiresIn` seconds.
683
+ *
684
+ * @example
685
+ * ```ts
686
+ * const { data } = await linabase.storage
687
+ * .from("docs")
688
+ * .createSignedUrl(`${userId}/report.pdf`, 3600);
689
+ * if (data) window.open(data.signedUrl);
690
+ * ```
691
+ */
588
692
  async createSignedUrl(path, expiresIn) {
589
693
  try {
590
694
  const res = await this.request(
@@ -755,6 +859,22 @@ var AuthClient = class {
755
859
  }
756
860
  }
757
861
  // ─── Email/Password ────────────────────────────────────────
862
+ /**
863
+ * Create a new user account with email + password.
864
+ *
865
+ * On success, the user is signed in and the session is persisted. Pass
866
+ * extra metadata via `data`; it is stored on the user record and
867
+ * available later as `user.user_metadata`.
868
+ *
869
+ * @example
870
+ * ```ts
871
+ * const { data, error } = await linabase.auth.signUp({
872
+ * email: "alice@example.com",
873
+ * password: "correct horse battery staple",
874
+ * data: { full_name: "Alice" },
875
+ * });
876
+ * ```
877
+ */
758
878
  async signUp(params) {
759
879
  try {
760
880
  const res = await this.request("/auth/v1/signup", {
@@ -771,6 +891,21 @@ var AuthClient = class {
771
891
  return { data: null, error: { message: err.message } };
772
892
  }
773
893
  }
894
+ /**
895
+ * Sign in an existing user with email + password.
896
+ *
897
+ * Alias of {@link signInWithPassword}. On success, the session is persisted
898
+ * and subsequent SDK calls use the returned access token automatically.
899
+ *
900
+ * @example
901
+ * ```ts
902
+ * const { data, error } = await linabase.auth.signIn({
903
+ * email: "alice@example.com",
904
+ * password: "correct horse battery staple",
905
+ * });
906
+ * if (error) console.error(error.message);
907
+ * ```
908
+ */
774
909
  async signIn(params) {
775
910
  try {
776
911
  const res = await this.request("/auth/v1/token?grant_type=password", {
@@ -812,6 +947,105 @@ var AuthClient = class {
812
947
  return { data: null, error: { message: err.message } };
813
948
  }
814
949
  }
950
+ // ─── Device-bound (Mobile) ──────────────────────────────────
951
+ /**
952
+ * Sign in with a device-bound secret. Intended for mobile clients that
953
+ * generate a random secret on first launch and persist it in the device
954
+ * keychain. No email or password is involved.
955
+ *
956
+ * Pass `create: true` on first launch to register a new anonymous user.
957
+ * Subsequent launches should call without `create` (or with `create: false`)
958
+ * to refuse silent account creation if the keychain entry was lost.
959
+ *
960
+ * Requires "Device-bound sign-ins" to be enabled in the project's Auth
961
+ * Settings.
962
+ *
963
+ * @example
964
+ * ```ts
965
+ * const secret = getOrCreateDeviceSecret();
966
+ * const { data, error } = await linabase.auth.signInWithDevice({
967
+ * deviceSecret: secret,
968
+ * create: true,
969
+ * });
970
+ * ```
971
+ */
972
+ async signInWithDevice(params) {
973
+ try {
974
+ const res = await this.request("/auth/v1/token?grant_type=device", {
975
+ method: "POST",
976
+ body: JSON.stringify({
977
+ device_secret: params.deviceSecret,
978
+ create: params.create,
979
+ data: params.data
980
+ })
981
+ });
982
+ const data = await res.json();
983
+ if (!res.ok) return { data: null, error: data };
984
+ const session = this.parseAuthResponse(data);
985
+ this.setSession(session, true);
986
+ this.emit("SIGNED_IN", session);
987
+ return { data: session, error: null };
988
+ } catch (err) {
989
+ return { data: null, error: { message: err.message } };
990
+ }
991
+ }
992
+ // ─── Game Center (iOS) ──────────────────────────────────────
993
+ /**
994
+ * Sign in with an Apple Game Center identity assertion. The mobile client
995
+ * obtains the assertion fields via
996
+ * `GKLocalPlayer.local.fetchItems(forIdentityVerificationSignature:)` (iOS
997
+ * 13.5+) and forwards them here. The project must have the app's bundle ID
998
+ * configured in the Game Center allowlist.
999
+ */
1000
+ async signInWithGameCenter(params) {
1001
+ try {
1002
+ const res = await this.request("/auth/v1/token?grant_type=game_center", {
1003
+ method: "POST",
1004
+ body: JSON.stringify({
1005
+ player_id: params.playerId,
1006
+ bundle_id: params.bundleId,
1007
+ public_key_url: params.publicKeyURL,
1008
+ signature: params.signature,
1009
+ salt: params.salt,
1010
+ timestamp: params.timestamp,
1011
+ display_name: params.displayName
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
+ // ─── Google Play Games (Android) ────────────────────────────
1025
+ /**
1026
+ * Sign in with a Google Play Games server auth code obtained via
1027
+ * `PlayGamesSignInClient.requestServerSideAccess(serverClientId)`. The
1028
+ * project must have the matching OAuth client configured in Auth Settings.
1029
+ */
1030
+ async signInWithPlayGames(params) {
1031
+ try {
1032
+ const res = await this.request("/auth/v1/token?grant_type=play_games", {
1033
+ method: "POST",
1034
+ body: JSON.stringify({
1035
+ server_auth_code: params.serverAuthCode,
1036
+ redirect_uri: params.redirectUri
1037
+ })
1038
+ });
1039
+ const data = await res.json();
1040
+ if (!res.ok) return { data: null, error: data };
1041
+ const session = this.parseAuthResponse(data);
1042
+ this.setSession(session, true);
1043
+ this.emit("SIGNED_IN", session);
1044
+ return { data: session, error: null };
1045
+ } catch (err) {
1046
+ return { data: null, error: { message: err.message } };
1047
+ }
1048
+ }
815
1049
  // ─── OAuth ─────────────────────────────────────────────────
816
1050
  signInWithOAuth(params) {
817
1051
  const queryParams = new URLSearchParams({ provider: params.provider });
@@ -825,6 +1059,16 @@ var AuthClient = class {
825
1059
  return { url };
826
1060
  }
827
1061
  // ─── Session Management ────────────────────────────────────
1062
+ /**
1063
+ * Sign the current user out, clear the persisted session, and revoke the
1064
+ * refresh token on the server. Fires a `SIGNED_OUT` event to listeners
1065
+ * registered with `onAuthStateChange`.
1066
+ *
1067
+ * @example
1068
+ * ```ts
1069
+ * await linabase.auth.signOut();
1070
+ * ```
1071
+ */
828
1072
  async signOut() {
829
1073
  try {
830
1074
  await this.request("/auth/v1/logout", { method: "POST" });
@@ -835,6 +1079,17 @@ var AuthClient = class {
835
1079
  return { error: { message: err.message } };
836
1080
  }
837
1081
  }
1082
+ /**
1083
+ * Return the current persisted session. Auto-refreshes if the access
1084
+ * token is expired and a refresh token is available. Returns
1085
+ * `{ session: null }` when the user isn't signed in.
1086
+ *
1087
+ * @example
1088
+ * ```ts
1089
+ * const { data: { session } } = await linabase.auth.getSession();
1090
+ * if (session) console.log("Signed in as", session.user.email);
1091
+ * ```
1092
+ */
838
1093
  async getSession() {
839
1094
  if (this.currentSession) {
840
1095
  const now = Math.floor(Date.now() / 1e3);
@@ -1035,6 +1290,41 @@ var AuthClient = class {
1035
1290
  } catch (err) {
1036
1291
  return { error: { message: err.message } };
1037
1292
  }
1293
+ },
1294
+ async listSessions(userId) {
1295
+ try {
1296
+ const res = await request(`/auth/v1/admin/users/${userId}/sessions`);
1297
+ const data = await res.json();
1298
+ if (!res.ok) return { data: null, error: data };
1299
+ return { data: data.sessions, error: null };
1300
+ } catch (err) {
1301
+ return { data: null, error: { message: err.message } };
1302
+ }
1303
+ },
1304
+ async deleteSession(userId, sessionId) {
1305
+ try {
1306
+ const res = await request(`/auth/v1/admin/users/${userId}/sessions/${sessionId}`, {
1307
+ method: "DELETE"
1308
+ });
1309
+ const data = await res.json().catch(() => ({}));
1310
+ if (!res.ok) return { data: null, error: data };
1311
+ return { data, error: null };
1312
+ } catch (err) {
1313
+ return { data: null, error: { message: err.message } };
1314
+ }
1315
+ },
1316
+ async deleteOtherSessions(userId, exceptSessionId) {
1317
+ try {
1318
+ const qs = exceptSessionId ? `?except=${encodeURIComponent(exceptSessionId)}` : "";
1319
+ const res = await request(`/auth/v1/admin/users/${userId}/sessions${qs}`, {
1320
+ method: "DELETE"
1321
+ });
1322
+ const data = await res.json().catch(() => ({}));
1323
+ if (!res.ok) return { data: null, error: data };
1324
+ return { data, error: null };
1325
+ } catch (err) {
1326
+ return { data: null, error: { message: err.message } };
1327
+ }
1038
1328
  }
1039
1329
  };
1040
1330
  }
@@ -1148,6 +1438,19 @@ var FunctionsClient = class {
1148
1438
  constructor(request) {
1149
1439
  this.request = request;
1150
1440
  }
1441
+ /**
1442
+ * Call a function by name.
1443
+ *
1444
+ * @param name - Function slug as registered on the project.
1445
+ * @param options - Body, method, and optional extra headers.
1446
+ * @returns `{ data, error }` — `data` is the parsed JSON response, `error`
1447
+ * is an object with `message` if the call failed (HTTP non-2xx or thrown).
1448
+ *
1449
+ * @example
1450
+ * ```ts
1451
+ * await linabase.functions.invoke("ping");
1452
+ * ```
1453
+ */
1151
1454
  async invoke(name, options) {
1152
1455
  try {
1153
1456
  const method = options?.method || "POST";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linabase/js",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "JavaScript/TypeScript client SDK for Linabase (database, storage, auth)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,7 +43,9 @@
43
43
  "test:watch": "vitest",
44
44
  "test:coverage": "vitest run --coverage",
45
45
  "typecheck": "tsc --noEmit",
46
- "lint": "eslint src/"
46
+ "lint": "eslint src/",
47
+ "docs:json": "typedoc --json dist/typedoc.json",
48
+ "docs:reference": "pnpm docs:json && node scripts/build-reference.mjs"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@linabase/db": "workspace:*",
@@ -52,6 +54,7 @@
52
54
  "@vitest/coverage-v8": "^3.2.4",
53
55
  "pg": "^8.13.0",
54
56
  "tsup": "^8.3.0",
57
+ "typedoc": "^0.28.19",
55
58
  "typescript": "^5.7.0",
56
59
  "vitest": "^3.2.1"
57
60
  }