@linabase/js 0.3.0 → 0.4.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.cjs CHANGED
@@ -52,16 +52,16 @@ var DatabaseClient = class {
52
52
  // ─── Query Methods ──────────────────────────────────────
53
53
  /** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
54
54
  select(columns, options) {
55
- this.method = "GET";
56
- if (columns) this.params.set("select", columns);
55
+ this.method = options?.head ? "HEAD" : "GET";
56
+ if (columns && columns !== "*") this.params.set("select", columns);
57
57
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
58
- if (options?.head) this.method = "GET";
59
58
  return this;
60
59
  }
61
- insert(data) {
60
+ insert(data, options) {
62
61
  this.method = "POST";
63
62
  this.body = data;
64
63
  this.preferHeaders.push("return=representation");
64
+ if (options?.defaultToNull === false) this.preferHeaders.push("missing=default");
65
65
  return this;
66
66
  }
67
67
  /** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
@@ -75,6 +75,7 @@ var DatabaseClient = class {
75
75
  if (options?.onConflict) {
76
76
  this.params.set("on_conflict", options.onConflict);
77
77
  }
78
+ if (options?.defaultToNull === false) this.preferHeaders.push("missing=default");
78
79
  return this;
79
80
  }
80
81
  update(data) {
@@ -83,8 +84,28 @@ var DatabaseClient = class {
83
84
  this.preferHeaders.push("return=representation");
84
85
  return this;
85
86
  }
86
- delete() {
87
+ delete(options) {
87
88
  this.method = "DELETE";
89
+ if (options?.count) this.preferHeaders.push(`count=${options.count}`);
90
+ return this;
91
+ }
92
+ /** Specify return=minimal to skip returning data on mutations (better performance). */
93
+ returning(mode = "representation") {
94
+ this.preferHeaders = this.preferHeaders.filter((h) => !h.startsWith("return="));
95
+ this.preferHeaders.push(`return=${mode}`);
96
+ return this;
97
+ }
98
+ /** Get the query execution plan (EXPLAIN). */
99
+ explain(options) {
100
+ const parts = ["plan"];
101
+ if (options?.analyze) parts.push("analyze");
102
+ if (options?.verbose) parts.push("verbose");
103
+ if (options?.settings) parts.push("settings");
104
+ if (options?.buffers) parts.push("buffers");
105
+ if (options?.wal) parts.push("wal");
106
+ if (options?.format) parts.push(`format:${options.format}`);
107
+ this.preferHeaders = this.preferHeaders.filter((h) => !h.startsWith("plan"));
108
+ this.preferHeaders.push(parts.join("|"));
88
109
  return this;
89
110
  }
90
111
  // ─── Comparison Filters ─────────────────────────────────
@@ -168,6 +189,32 @@ var DatabaseClient = class {
168
189
  this.params.set(column, `isdistinct.${value}`);
169
190
  return this;
170
191
  }
192
+ // ─── Range Type Filters ──────────────────────────────────
193
+ /** Strictly left of: [a,b] << [c,d] */
194
+ rangeLt(column, range) {
195
+ this.params.set(column, `sl.${range}`);
196
+ return this;
197
+ }
198
+ /** Strictly right of: [a,b] >> [c,d] */
199
+ rangeGt(column, range) {
200
+ this.params.set(column, `sr.${range}`);
201
+ return this;
202
+ }
203
+ /** Does not extend to the left of */
204
+ rangeGte(column, range) {
205
+ this.params.set(column, `nxl.${range}`);
206
+ return this;
207
+ }
208
+ /** Does not extend to the right of */
209
+ rangeLte(column, range) {
210
+ this.params.set(column, `nxr.${range}`);
211
+ return this;
212
+ }
213
+ /** Range is adjacent to */
214
+ rangeAdjacent(column, range) {
215
+ this.params.set(column, `adj.${range}`);
216
+ return this;
217
+ }
171
218
  // ─── Negation ───────────────────────────────────────────
172
219
  /** Negate a filter: not.eq, not.in, not.is, etc. */
173
220
  not(column, operator, value) {
@@ -176,29 +223,53 @@ var DatabaseClient = class {
176
223
  }
177
224
  // ─── Logical ────────────────────────────────────────────
178
225
  /** OR filter: or("age.gt.20,name.eq.John") */
179
- or(filters) {
180
- this.params.set("or", `(${filters})`);
226
+ or(filters, options) {
227
+ const key = options?.foreignTable ? `${options.foreignTable}.or` : "or";
228
+ this.params.set(key, `(${filters})`);
229
+ return this;
230
+ }
231
+ /** AND filter: and("age.gt.20,name.eq.John") */
232
+ and(filters, options) {
233
+ const key = options?.foreignTable ? `${options.foreignTable}.and` : "and";
234
+ this.params.set(key, `(${filters})`);
235
+ return this;
236
+ }
237
+ /** Generic filter: filter("column", "operator", "value") */
238
+ filter(column, operator, value) {
239
+ this.params.set(column, `${operator}.${value}`);
181
240
  return this;
182
241
  }
183
242
  // ─── Full-Text Search ───────────────────────────────────
184
- /** to_tsquery */
243
+ /** Full-text search (to_tsquery, plainto_tsquery, phraseto_tsquery, or websearch_to_tsquery) */
185
244
  textSearch(column, query, options) {
186
245
  const op = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : options?.type === "websearch" ? "wfts" : "fts";
187
- this.params.set(column, `${op}.${query}`);
246
+ const config = options?.config ? `(${options.config})` : "";
247
+ this.params.set(column, `${op}${config}.${query}`);
188
248
  return this;
189
249
  }
190
250
  // ─── Ordering & Pagination ──────────────────────────────
191
251
  order(column, options) {
192
252
  const dir = options?.ascending === false ? "desc" : "asc";
193
253
  const nulls = options?.nullsFirst === true ? ".nullsfirst" : options?.nullsFirst === false ? ".nullslast" : "";
194
- const existing = this.params.get("order");
254
+ const key = options?.foreignTable ? `${options.foreignTable}.order` : "order";
255
+ const existing = this.params.get(key);
195
256
  if (existing) {
196
- this.params.set("order", `${existing},${column}.${dir}${nulls}`);
257
+ this.params.set(key, `${existing},${column}.${dir}${nulls}`);
197
258
  } else {
198
- this.params.set("order", `${column}.${dir}${nulls}`);
259
+ this.params.set(key, `${column}.${dir}${nulls}`);
199
260
  }
200
261
  return this;
201
262
  }
263
+ /** Limit results on a foreign table */
264
+ limitForeignTable(count, foreignTable) {
265
+ this.params.set(`${foreignTable}.limit`, String(count));
266
+ return this;
267
+ }
268
+ /** Offset results on a foreign table */
269
+ offsetForeignTable(count, foreignTable) {
270
+ this.params.set(`${foreignTable}.offset`, String(count));
271
+ return this;
272
+ }
202
273
  limit(count) {
203
274
  this.params.set("limit", String(count));
204
275
  return this;
@@ -269,6 +340,7 @@ var DatabaseClient = class {
269
340
  async execute() {
270
341
  const qs = this.params.toString();
271
342
  const path = `/rest/v1/${this.table}${qs ? `?${qs}` : ""}`;
343
+ const isHead = this.method === "HEAD";
272
344
  const isSingle = this._single;
273
345
  const isMaybeSingle = this._maybeSingle;
274
346
  const shouldThrow = this._throwOnError;
@@ -301,8 +373,11 @@ var DatabaseClient = class {
301
373
  body: this.body ? JSON.stringify(this.body) : void 0,
302
374
  signal: abortSignal
303
375
  });
304
- const countHeader = res.headers.get("X-Total-Count");
376
+ const countHeader = res.headers.get("X-Total-Count") || res.headers.get("content-range")?.split("/")[1];
305
377
  const totalCount = countHeader ? parseInt(countHeader) : null;
378
+ if (isHead) {
379
+ return { data: null, error: res.ok ? null : { message: res.statusText }, count: totalCount };
380
+ }
306
381
  if (isCsv) {
307
382
  if (!res.ok) {
308
383
  const text = await res.text();
package/dist/index.d.cts CHANGED
@@ -39,14 +39,30 @@ declare class DatabaseClient {
39
39
  count?: "exact" | "estimated" | "planned";
40
40
  head?: boolean;
41
41
  }): this;
42
- insert(data: Record<string, any> | Record<string, any>[]): this;
42
+ insert(data: Record<string, any> | Record<string, any>[], options?: {
43
+ defaultToNull?: boolean;
44
+ }): this;
43
45
  /** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
44
46
  upsert(data: Record<string, any> | Record<string, any>[], options?: {
45
47
  ignoreDuplicates?: boolean;
46
48
  onConflict?: string;
49
+ defaultToNull?: boolean;
47
50
  }): this;
48
51
  update(data: Record<string, any>): this;
49
- delete(): this;
52
+ delete(options?: {
53
+ count?: "exact" | "estimated" | "planned";
54
+ }): this;
55
+ /** Specify return=minimal to skip returning data on mutations (better performance). */
56
+ returning(mode?: "minimal" | "representation"): this;
57
+ /** Get the query execution plan (EXPLAIN). */
58
+ explain(options?: {
59
+ analyze?: boolean;
60
+ verbose?: boolean;
61
+ settings?: boolean;
62
+ buffers?: boolean;
63
+ wal?: boolean;
64
+ format?: "json" | "text";
65
+ }): this;
50
66
  eq(column: string, value: any): this;
51
67
  neq(column: string, value: any): this;
52
68
  gt(column: string, value: any): this;
@@ -69,18 +85,42 @@ declare class DatabaseClient {
69
85
  is(column: string, value: "null" | "true" | "false" | null | boolean): this;
70
86
  /** IS DISTINCT FROM */
71
87
  isDistinct(column: string, value: any): this;
88
+ /** Strictly left of: [a,b] << [c,d] */
89
+ rangeLt(column: string, range: string): this;
90
+ /** Strictly right of: [a,b] >> [c,d] */
91
+ rangeGt(column: string, range: string): this;
92
+ /** Does not extend to the left of */
93
+ rangeGte(column: string, range: string): this;
94
+ /** Does not extend to the right of */
95
+ rangeLte(column: string, range: string): this;
96
+ /** Range is adjacent to */
97
+ rangeAdjacent(column: string, range: string): this;
72
98
  /** Negate a filter: not.eq, not.in, not.is, etc. */
73
99
  not(column: string, operator: string, value: any): this;
74
100
  /** OR filter: or("age.gt.20,name.eq.John") */
75
- or(filters: string): this;
76
- /** to_tsquery */
101
+ or(filters: string, options?: {
102
+ foreignTable?: string;
103
+ }): this;
104
+ /** AND filter: and("age.gt.20,name.eq.John") */
105
+ and(filters: string, options?: {
106
+ foreignTable?: string;
107
+ }): this;
108
+ /** Generic filter: filter("column", "operator", "value") */
109
+ filter(column: string, operator: string, value: any): this;
110
+ /** Full-text search (to_tsquery, plainto_tsquery, phraseto_tsquery, or websearch_to_tsquery) */
77
111
  textSearch(column: string, query: string, options?: {
78
112
  type?: "plain" | "phrase" | "websearch";
113
+ config?: string;
79
114
  }): this;
80
115
  order(column: string, options?: {
81
116
  ascending?: boolean;
82
117
  nullsFirst?: boolean;
118
+ foreignTable?: string;
83
119
  }): this;
120
+ /** Limit results on a foreign table */
121
+ limitForeignTable(count: number, foreignTable: string): this;
122
+ /** Offset results on a foreign table */
123
+ offsetForeignTable(count: number, foreignTable: string): this;
84
124
  limit(count: number): this;
85
125
  offset(count: number): this;
86
126
  /** Request exact, estimated, or planned count via Prefer header */
package/dist/index.d.ts CHANGED
@@ -39,14 +39,30 @@ declare class DatabaseClient {
39
39
  count?: "exact" | "estimated" | "planned";
40
40
  head?: boolean;
41
41
  }): this;
42
- insert(data: Record<string, any> | Record<string, any>[]): this;
42
+ insert(data: Record<string, any> | Record<string, any>[], options?: {
43
+ defaultToNull?: boolean;
44
+ }): this;
43
45
  /** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
44
46
  upsert(data: Record<string, any> | Record<string, any>[], options?: {
45
47
  ignoreDuplicates?: boolean;
46
48
  onConflict?: string;
49
+ defaultToNull?: boolean;
47
50
  }): this;
48
51
  update(data: Record<string, any>): this;
49
- delete(): this;
52
+ delete(options?: {
53
+ count?: "exact" | "estimated" | "planned";
54
+ }): this;
55
+ /** Specify return=minimal to skip returning data on mutations (better performance). */
56
+ returning(mode?: "minimal" | "representation"): this;
57
+ /** Get the query execution plan (EXPLAIN). */
58
+ explain(options?: {
59
+ analyze?: boolean;
60
+ verbose?: boolean;
61
+ settings?: boolean;
62
+ buffers?: boolean;
63
+ wal?: boolean;
64
+ format?: "json" | "text";
65
+ }): this;
50
66
  eq(column: string, value: any): this;
51
67
  neq(column: string, value: any): this;
52
68
  gt(column: string, value: any): this;
@@ -69,18 +85,42 @@ declare class DatabaseClient {
69
85
  is(column: string, value: "null" | "true" | "false" | null | boolean): this;
70
86
  /** IS DISTINCT FROM */
71
87
  isDistinct(column: string, value: any): this;
88
+ /** Strictly left of: [a,b] << [c,d] */
89
+ rangeLt(column: string, range: string): this;
90
+ /** Strictly right of: [a,b] >> [c,d] */
91
+ rangeGt(column: string, range: string): this;
92
+ /** Does not extend to the left of */
93
+ rangeGte(column: string, range: string): this;
94
+ /** Does not extend to the right of */
95
+ rangeLte(column: string, range: string): this;
96
+ /** Range is adjacent to */
97
+ rangeAdjacent(column: string, range: string): this;
72
98
  /** Negate a filter: not.eq, not.in, not.is, etc. */
73
99
  not(column: string, operator: string, value: any): this;
74
100
  /** OR filter: or("age.gt.20,name.eq.John") */
75
- or(filters: string): this;
76
- /** to_tsquery */
101
+ or(filters: string, options?: {
102
+ foreignTable?: string;
103
+ }): this;
104
+ /** AND filter: and("age.gt.20,name.eq.John") */
105
+ and(filters: string, options?: {
106
+ foreignTable?: string;
107
+ }): this;
108
+ /** Generic filter: filter("column", "operator", "value") */
109
+ filter(column: string, operator: string, value: any): this;
110
+ /** Full-text search (to_tsquery, plainto_tsquery, phraseto_tsquery, or websearch_to_tsquery) */
77
111
  textSearch(column: string, query: string, options?: {
78
112
  type?: "plain" | "phrase" | "websearch";
113
+ config?: string;
79
114
  }): this;
80
115
  order(column: string, options?: {
81
116
  ascending?: boolean;
82
117
  nullsFirst?: boolean;
118
+ foreignTable?: string;
83
119
  }): this;
120
+ /** Limit results on a foreign table */
121
+ limitForeignTable(count: number, foreignTable: string): this;
122
+ /** Offset results on a foreign table */
123
+ offsetForeignTable(count: number, foreignTable: string): this;
84
124
  limit(count: number): this;
85
125
  offset(count: number): this;
86
126
  /** Request exact, estimated, or planned count via Prefer header */
package/dist/index.js CHANGED
@@ -20,16 +20,16 @@ var DatabaseClient = class {
20
20
  // ─── Query Methods ──────────────────────────────────────
21
21
  /** Select columns. Supports nested joins: "*, comments(*)" and aliases: "full_name:name" */
22
22
  select(columns, options) {
23
- this.method = "GET";
24
- if (columns) this.params.set("select", columns);
23
+ this.method = options?.head ? "HEAD" : "GET";
24
+ if (columns && columns !== "*") this.params.set("select", columns);
25
25
  if (options?.count) this.preferHeaders.push(`count=${options.count}`);
26
- if (options?.head) this.method = "GET";
27
26
  return this;
28
27
  }
29
- insert(data) {
28
+ insert(data, options) {
30
29
  this.method = "POST";
31
30
  this.body = data;
32
31
  this.preferHeaders.push("return=representation");
32
+ if (options?.defaultToNull === false) this.preferHeaders.push("missing=default");
33
33
  return this;
34
34
  }
35
35
  /** Upsert: insert or update on conflict. Uses merge-duplicates by default. */
@@ -43,6 +43,7 @@ var DatabaseClient = class {
43
43
  if (options?.onConflict) {
44
44
  this.params.set("on_conflict", options.onConflict);
45
45
  }
46
+ if (options?.defaultToNull === false) this.preferHeaders.push("missing=default");
46
47
  return this;
47
48
  }
48
49
  update(data) {
@@ -51,8 +52,28 @@ var DatabaseClient = class {
51
52
  this.preferHeaders.push("return=representation");
52
53
  return this;
53
54
  }
54
- delete() {
55
+ delete(options) {
55
56
  this.method = "DELETE";
57
+ if (options?.count) this.preferHeaders.push(`count=${options.count}`);
58
+ return this;
59
+ }
60
+ /** Specify return=minimal to skip returning data on mutations (better performance). */
61
+ returning(mode = "representation") {
62
+ this.preferHeaders = this.preferHeaders.filter((h) => !h.startsWith("return="));
63
+ this.preferHeaders.push(`return=${mode}`);
64
+ return this;
65
+ }
66
+ /** Get the query execution plan (EXPLAIN). */
67
+ explain(options) {
68
+ const parts = ["plan"];
69
+ if (options?.analyze) parts.push("analyze");
70
+ if (options?.verbose) parts.push("verbose");
71
+ if (options?.settings) parts.push("settings");
72
+ if (options?.buffers) parts.push("buffers");
73
+ if (options?.wal) parts.push("wal");
74
+ if (options?.format) parts.push(`format:${options.format}`);
75
+ this.preferHeaders = this.preferHeaders.filter((h) => !h.startsWith("plan"));
76
+ this.preferHeaders.push(parts.join("|"));
56
77
  return this;
57
78
  }
58
79
  // ─── Comparison Filters ─────────────────────────────────
@@ -136,6 +157,32 @@ var DatabaseClient = class {
136
157
  this.params.set(column, `isdistinct.${value}`);
137
158
  return this;
138
159
  }
160
+ // ─── Range Type Filters ──────────────────────────────────
161
+ /** Strictly left of: [a,b] << [c,d] */
162
+ rangeLt(column, range) {
163
+ this.params.set(column, `sl.${range}`);
164
+ return this;
165
+ }
166
+ /** Strictly right of: [a,b] >> [c,d] */
167
+ rangeGt(column, range) {
168
+ this.params.set(column, `sr.${range}`);
169
+ return this;
170
+ }
171
+ /** Does not extend to the left of */
172
+ rangeGte(column, range) {
173
+ this.params.set(column, `nxl.${range}`);
174
+ return this;
175
+ }
176
+ /** Does not extend to the right of */
177
+ rangeLte(column, range) {
178
+ this.params.set(column, `nxr.${range}`);
179
+ return this;
180
+ }
181
+ /** Range is adjacent to */
182
+ rangeAdjacent(column, range) {
183
+ this.params.set(column, `adj.${range}`);
184
+ return this;
185
+ }
139
186
  // ─── Negation ───────────────────────────────────────────
140
187
  /** Negate a filter: not.eq, not.in, not.is, etc. */
141
188
  not(column, operator, value) {
@@ -144,29 +191,53 @@ var DatabaseClient = class {
144
191
  }
145
192
  // ─── Logical ────────────────────────────────────────────
146
193
  /** OR filter: or("age.gt.20,name.eq.John") */
147
- or(filters) {
148
- this.params.set("or", `(${filters})`);
194
+ or(filters, options) {
195
+ const key = options?.foreignTable ? `${options.foreignTable}.or` : "or";
196
+ this.params.set(key, `(${filters})`);
197
+ return this;
198
+ }
199
+ /** AND filter: and("age.gt.20,name.eq.John") */
200
+ and(filters, options) {
201
+ const key = options?.foreignTable ? `${options.foreignTable}.and` : "and";
202
+ this.params.set(key, `(${filters})`);
203
+ return this;
204
+ }
205
+ /** Generic filter: filter("column", "operator", "value") */
206
+ filter(column, operator, value) {
207
+ this.params.set(column, `${operator}.${value}`);
149
208
  return this;
150
209
  }
151
210
  // ─── Full-Text Search ───────────────────────────────────
152
- /** to_tsquery */
211
+ /** Full-text search (to_tsquery, plainto_tsquery, phraseto_tsquery, or websearch_to_tsquery) */
153
212
  textSearch(column, query, options) {
154
213
  const op = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : options?.type === "websearch" ? "wfts" : "fts";
155
- this.params.set(column, `${op}.${query}`);
214
+ const config = options?.config ? `(${options.config})` : "";
215
+ this.params.set(column, `${op}${config}.${query}`);
156
216
  return this;
157
217
  }
158
218
  // ─── Ordering & Pagination ──────────────────────────────
159
219
  order(column, options) {
160
220
  const dir = options?.ascending === false ? "desc" : "asc";
161
221
  const nulls = options?.nullsFirst === true ? ".nullsfirst" : options?.nullsFirst === false ? ".nullslast" : "";
162
- const existing = this.params.get("order");
222
+ const key = options?.foreignTable ? `${options.foreignTable}.order` : "order";
223
+ const existing = this.params.get(key);
163
224
  if (existing) {
164
- this.params.set("order", `${existing},${column}.${dir}${nulls}`);
225
+ this.params.set(key, `${existing},${column}.${dir}${nulls}`);
165
226
  } else {
166
- this.params.set("order", `${column}.${dir}${nulls}`);
227
+ this.params.set(key, `${column}.${dir}${nulls}`);
167
228
  }
168
229
  return this;
169
230
  }
231
+ /** Limit results on a foreign table */
232
+ limitForeignTable(count, foreignTable) {
233
+ this.params.set(`${foreignTable}.limit`, String(count));
234
+ return this;
235
+ }
236
+ /** Offset results on a foreign table */
237
+ offsetForeignTable(count, foreignTable) {
238
+ this.params.set(`${foreignTable}.offset`, String(count));
239
+ return this;
240
+ }
170
241
  limit(count) {
171
242
  this.params.set("limit", String(count));
172
243
  return this;
@@ -237,6 +308,7 @@ var DatabaseClient = class {
237
308
  async execute() {
238
309
  const qs = this.params.toString();
239
310
  const path = `/rest/v1/${this.table}${qs ? `?${qs}` : ""}`;
311
+ const isHead = this.method === "HEAD";
240
312
  const isSingle = this._single;
241
313
  const isMaybeSingle = this._maybeSingle;
242
314
  const shouldThrow = this._throwOnError;
@@ -269,8 +341,11 @@ var DatabaseClient = class {
269
341
  body: this.body ? JSON.stringify(this.body) : void 0,
270
342
  signal: abortSignal
271
343
  });
272
- const countHeader = res.headers.get("X-Total-Count");
344
+ const countHeader = res.headers.get("X-Total-Count") || res.headers.get("content-range")?.split("/")[1];
273
345
  const totalCount = countHeader ? parseInt(countHeader) : null;
346
+ if (isHead) {
347
+ return { data: null, error: res.ok ? null : { message: res.statusText }, count: totalCount };
348
+ }
274
349
  if (isCsv) {
275
350
  if (!res.ok) {
276
351
  const text = await res.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linabase/js",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "JavaScript/TypeScript client SDK for Linabase (database, storage, auth)",
5
5
  "license": "MIT",
6
6
  "type": "module",