@luxdb/sdk 2.0.0 → 2.2.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/README.md CHANGED
@@ -80,17 +80,32 @@ const { data: matches } = await lux
80
80
  .near("embedding", queryEmbedding, { k: 10, threshold: 0.8 });
81
81
  ```
82
82
 
83
+ Writes return the affected row(s), including server-generated columns (`id`,
84
+ UUIDv7 primary keys, `DEFAULT now()` timestamps):
85
+
83
86
  ```ts
87
+ // insert -> the inserted row
84
88
  const { data: inserted, error: insertError } = await lux
85
89
  .table("messages")
86
90
  .insert({ body: "hello", channel: "general" });
87
91
 
88
- const { data: updated, error: updateError } = await lux
92
+ // bulk insert in a single request -> array of rows
93
+ const { data: many } = await lux
94
+ .table("messages")
95
+ .insert([{ body: "a" }, { body: "b" }]);
96
+
97
+ // upsert: insert, or update the row that conflicts on `onConflict` (default: PK)
98
+ const { data: user } = await lux
99
+ .table("users")
100
+ .upsert({ email: "a@x.com", name: "Bob" }, { onConflict: "email" });
101
+
102
+ // update / delete -> the affected rows
103
+ const { data: updated } = await lux
89
104
  .table("messages")
90
105
  .update({ body: "edited" })
91
106
  .eq("id", inserted?.id);
92
107
 
93
- const { data: deleted, error: deleteError } = await lux
108
+ const { data: deleted } = await lux
94
109
  .table("messages")
95
110
  .delete()
96
111
  .eq("id", inserted?.id);
@@ -116,6 +131,10 @@ await lux.table("events").select().eq("metadata.plan.tier", "pro");
116
131
  await lux.table("events").select().isValid("metadata.count");
117
132
  await lux.table("events").select().isNotValid("metadata.deleted_at");
118
133
 
134
+ // IS NULL / IS NOT NULL on a regular column (NULL == the column is absent)
135
+ await lux.table("tasks").select().isNull("deleted_at");
136
+ await lux.table("tasks").select().isNotNull("archived_at");
137
+
119
138
  // Array membership, and a declared JSON-path index for range queries at scale.
120
139
  await lux.table("events").select().contains("tags", "urgent");
121
140
  await lux.table("events").createIndex("metadata.plan.tier", "str");
@@ -218,6 +218,12 @@ class LuxProjectTable {
218
218
  insert(rowOrRows) {
219
219
  return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
220
220
  }
221
+ upsert(rowOrRows, options) {
222
+ return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows, {
223
+ upsert: true,
224
+ onConflict: options?.onConflict,
225
+ });
226
+ }
221
227
  update(patch) {
222
228
  return new LuxProjectMutationBuilder(this.client, this.name, 'PATCH', patch);
223
229
  }
@@ -272,8 +278,17 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
272
278
  return this.addFilter(column, 'lte', value);
273
279
  }
274
280
  is(column, value) {
281
+ // `.is(col, null)` is the Supabase-style spelling of an IS NULL check.
282
+ if (value === null)
283
+ return this.addFilter(column, 'isNull', '');
275
284
  return this.addFilter(column, 'is', value);
276
285
  }
286
+ isNull(column) {
287
+ return this.addFilter(column, 'isNull', '');
288
+ }
289
+ isNotNull(column) {
290
+ return this.addFilter(column, 'isNotNull', '');
291
+ }
277
292
  in(column, values) {
278
293
  return this.addFilter(column, 'in', values);
279
294
  }
@@ -589,24 +604,30 @@ class LuxProjectLiveSubscription {
589
604
  }
590
605
  exports.LuxProjectLiveSubscription = LuxProjectLiveSubscription;
591
606
  class LuxProjectInsertBuilder extends LuxProjectThenable {
592
- constructor(client, tableName, rowOrRows) {
607
+ constructor(client, tableName, rowOrRows, upsertOptions) {
593
608
  super();
594
609
  this.client = client;
595
610
  this.tableName = tableName;
596
611
  this.rowOrRows = rowOrRows;
612
+ this.upsertOptions = upsertOptions;
597
613
  }
598
614
  async execute() {
599
- if (!Array.isArray(this.rowOrRows)) {
600
- return this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, this.rowOrRows);
601
- }
602
- const results = [];
603
- for (const row of this.rowOrRows) {
604
- const result = await this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, row);
605
- if (result.error)
606
- return result;
607
- results.push(result.data);
615
+ // One request for both shapes: an array body inserts all rows server-side
616
+ // in a single round-trip. The server returns the affected row(s)
617
+ // ({result: row} for a single row, {result: [rows]} for an array).
618
+ let path = `/tables/${encodeURIComponent(this.tableName)}`;
619
+ if (this.upsertOptions?.upsert) {
620
+ const params = new URLSearchParams();
621
+ if (this.upsertOptions.onConflict)
622
+ params.set('on_conflict', this.upsertOptions.onConflict);
623
+ else
624
+ params.set('upsert', 'true');
625
+ path += `?${params.toString()}`;
608
626
  }
609
- return (0, utils_1.ok)(results);
627
+ const res = await this.client.request('POST', path, this.rowOrRows);
628
+ if (res.error)
629
+ return res;
630
+ return (0, utils_1.ok)(unwrapResult(res.data));
610
631
  }
611
632
  }
612
633
  exports.LuxProjectInsertBuilder = LuxProjectInsertBuilder;
@@ -622,7 +643,11 @@ class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
622
643
  }
623
644
  const params = this.filteredQueryParams();
624
645
  const query = params.toString();
625
- return this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
646
+ // Update/delete return the affected rows ({result: [rows]}); unwrap them.
647
+ const res = await this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
648
+ if (res.error)
649
+ return res;
650
+ return (0, utils_1.ok)(unwrapResult(res.data));
626
651
  }
627
652
  }
628
653
  exports.LuxProjectMutationBuilder = LuxProjectMutationBuilder;
@@ -650,7 +675,10 @@ function filtersToWhere(filters) {
650
675
  const values = Array.isArray(filter.value) ? filter.value : [filter.value];
651
676
  return normalizeWhere(`${filter.column} ${op} ( ${values.map(formatWhereValue).join(' ')} )`);
652
677
  }
653
- if (filter.operator === 'isValid' || filter.operator === 'isNotValid') {
678
+ if (filter.operator === 'isValid' ||
679
+ filter.operator === 'isNotValid' ||
680
+ filter.operator === 'isNull' ||
681
+ filter.operator === 'isNotNull') {
654
682
  return normalizeWhere(`${filter.column} ${op}`);
655
683
  }
656
684
  return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
@@ -685,6 +713,10 @@ function filterOperatorToWhere(operator) {
685
713
  return 'IS VALID';
686
714
  case 'isNotValid':
687
715
  return 'IS NOT VALID';
716
+ case 'isNull':
717
+ return 'IS NULL';
718
+ case 'isNotNull':
719
+ return 'IS NOT NULL';
688
720
  case 'contains':
689
721
  return 'CONTAINS';
690
722
  }
package/dist/cjs/ssr.js CHANGED
@@ -3,21 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createServerClient = createServerClient;
4
4
  const project_1 = require("./project");
5
5
  const DEFAULT_COOKIE = 'lux-auth-session';
6
- function createServerClient(url, key, options) {
6
+ function createServerClient(url, key, options = {}) {
7
7
  const storageKey = options.auth?.storageKey ?? DEFAULT_COOKIE;
8
8
  const cookieOptions = options.auth?.cookieOptions ?? {
9
9
  path: '/',
10
10
  sameSite: 'lax',
11
11
  };
12
12
  const { cookieOptions: _cookieOptions, ...authOptions } = options.auth ?? {};
13
+ // With cookies -> cookie-backed session (SSR). Without -> stateless backend
14
+ // client: no session storage, nothing to persist.
15
+ const hasCookies = options.cookies !== undefined;
13
16
  return (0, project_1.createClient)(url, key, {
14
17
  fetch: options.fetch,
15
18
  auth: {
16
- persistSession: true,
19
+ persistSession: hasCookies,
17
20
  autoRefreshToken: false,
18
21
  ...authOptions,
19
22
  storageKey,
20
- storage: cookieStorage(options.cookies, cookieOptions),
23
+ storage: hasCookies ? cookieStorage(options.cookies, cookieOptions) : null,
21
24
  },
22
25
  });
23
26
  }
@@ -212,6 +212,12 @@ export class LuxProjectTable {
212
212
  insert(rowOrRows) {
213
213
  return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows);
214
214
  }
215
+ upsert(rowOrRows, options) {
216
+ return new LuxProjectInsertBuilder(this.client, this.name, rowOrRows, {
217
+ upsert: true,
218
+ onConflict: options?.onConflict,
219
+ });
220
+ }
215
221
  update(patch) {
216
222
  return new LuxProjectMutationBuilder(this.client, this.name, 'PATCH', patch);
217
223
  }
@@ -265,8 +271,17 @@ class LuxProjectFilterBuilder extends LuxProjectThenable {
265
271
  return this.addFilter(column, 'lte', value);
266
272
  }
267
273
  is(column, value) {
274
+ // `.is(col, null)` is the Supabase-style spelling of an IS NULL check.
275
+ if (value === null)
276
+ return this.addFilter(column, 'isNull', '');
268
277
  return this.addFilter(column, 'is', value);
269
278
  }
279
+ isNull(column) {
280
+ return this.addFilter(column, 'isNull', '');
281
+ }
282
+ isNotNull(column) {
283
+ return this.addFilter(column, 'isNotNull', '');
284
+ }
270
285
  in(column, values) {
271
286
  return this.addFilter(column, 'in', values);
272
287
  }
@@ -580,24 +595,30 @@ export class LuxProjectLiveSubscription {
580
595
  }
581
596
  }
582
597
  export class LuxProjectInsertBuilder extends LuxProjectThenable {
583
- constructor(client, tableName, rowOrRows) {
598
+ constructor(client, tableName, rowOrRows, upsertOptions) {
584
599
  super();
585
600
  this.client = client;
586
601
  this.tableName = tableName;
587
602
  this.rowOrRows = rowOrRows;
603
+ this.upsertOptions = upsertOptions;
588
604
  }
589
605
  async execute() {
590
- if (!Array.isArray(this.rowOrRows)) {
591
- return this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, this.rowOrRows);
592
- }
593
- const results = [];
594
- for (const row of this.rowOrRows) {
595
- const result = await this.client.request('POST', `/tables/${encodeURIComponent(this.tableName)}`, row);
596
- if (result.error)
597
- return result;
598
- results.push(result.data);
606
+ // One request for both shapes: an array body inserts all rows server-side
607
+ // in a single round-trip. The server returns the affected row(s)
608
+ // ({result: row} for a single row, {result: [rows]} for an array).
609
+ let path = `/tables/${encodeURIComponent(this.tableName)}`;
610
+ if (this.upsertOptions?.upsert) {
611
+ const params = new URLSearchParams();
612
+ if (this.upsertOptions.onConflict)
613
+ params.set('on_conflict', this.upsertOptions.onConflict);
614
+ else
615
+ params.set('upsert', 'true');
616
+ path += `?${params.toString()}`;
599
617
  }
600
- return ok(results);
618
+ const res = await this.client.request('POST', path, this.rowOrRows);
619
+ if (res.error)
620
+ return res;
621
+ return ok(unwrapResult(res.data));
601
622
  }
602
623
  }
603
624
  export class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
@@ -612,7 +633,11 @@ export class LuxProjectMutationBuilder extends LuxProjectFilterBuilder {
612
633
  }
613
634
  const params = this.filteredQueryParams();
614
635
  const query = params.toString();
615
- return this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
636
+ // Update/delete return the affected rows ({result: [rows]}); unwrap them.
637
+ const res = await this.client.request(this.method, `/tables/${encodeURIComponent(this.tableName)}${query ? `?${query}` : ''}`, this.body);
638
+ if (res.error)
639
+ return res;
640
+ return ok(unwrapResult(res.data));
616
641
  }
617
642
  }
618
643
  function unwrapRows(payload) {
@@ -639,7 +664,10 @@ function filtersToWhere(filters) {
639
664
  const values = Array.isArray(filter.value) ? filter.value : [filter.value];
640
665
  return normalizeWhere(`${filter.column} ${op} ( ${values.map(formatWhereValue).join(' ')} )`);
641
666
  }
642
- if (filter.operator === 'isValid' || filter.operator === 'isNotValid') {
667
+ if (filter.operator === 'isValid' ||
668
+ filter.operator === 'isNotValid' ||
669
+ filter.operator === 'isNull' ||
670
+ filter.operator === 'isNotNull') {
643
671
  return normalizeWhere(`${filter.column} ${op}`);
644
672
  }
645
673
  return normalizeWhere(`${filter.column} ${op} ${formatWhereValue(filter.value)}`);
@@ -674,6 +702,10 @@ function filterOperatorToWhere(operator) {
674
702
  return 'IS VALID';
675
703
  case 'isNotValid':
676
704
  return 'IS NOT VALID';
705
+ case 'isNull':
706
+ return 'IS NULL';
707
+ case 'isNotNull':
708
+ return 'IS NOT NULL';
677
709
  case 'contains':
678
710
  return 'CONTAINS';
679
711
  }
package/dist/esm/ssr.js CHANGED
@@ -1,20 +1,23 @@
1
1
  import { createClient } from './project.js';
2
2
  const DEFAULT_COOKIE = 'lux-auth-session';
3
- export function createServerClient(url, key, options) {
3
+ export function createServerClient(url, key, options = {}) {
4
4
  const storageKey = options.auth?.storageKey ?? DEFAULT_COOKIE;
5
5
  const cookieOptions = options.auth?.cookieOptions ?? {
6
6
  path: '/',
7
7
  sameSite: 'lax',
8
8
  };
9
9
  const { cookieOptions: _cookieOptions, ...authOptions } = options.auth ?? {};
10
+ // With cookies -> cookie-backed session (SSR). Without -> stateless backend
11
+ // client: no session storage, nothing to persist.
12
+ const hasCookies = options.cookies !== undefined;
10
13
  return createClient(url, key, {
11
14
  fetch: options.fetch,
12
15
  auth: {
13
- persistSession: true,
16
+ persistSession: hasCookies,
14
17
  autoRefreshToken: false,
15
18
  ...authOptions,
16
19
  storageKey,
17
- storage: cookieStorage(options.cookies, cookieOptions),
20
+ storage: hasCookies ? cookieStorage(options.cookies, cookieOptions) : null,
18
21
  },
19
22
  });
20
23
  }
@@ -23,7 +23,7 @@ export interface LuxVectorSearchOptions {
23
23
  filter_value?: string;
24
24
  }
25
25
  type QueryValue = string | number | boolean | number[] | null;
26
- type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'is' | 'in' | 'notIn' | 'isValid' | 'isNotValid' | 'contains';
26
+ type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'is' | 'in' | 'notIn' | 'isValid' | 'isNotValid' | 'isNull' | 'isNotNull' | 'contains';
27
27
  type ProjectRowInput<T extends object> = Partial<T> & Record<string, QueryValue>;
28
28
  type ProjectSelectSingle<TResult> = TResult extends readonly (infer Row)[] ? Row : TResult;
29
29
  interface QueryFilter {
@@ -131,6 +131,12 @@ export declare class LuxProjectTable<T extends object> {
131
131
  live(): Promise<LuxLiveResult<T>>;
132
132
  insert(row: ProjectRowInput<T>): LuxProjectInsertBuilder<unknown>;
133
133
  insert(rows: Array<ProjectRowInput<T>>): LuxProjectInsertBuilder<unknown[]>;
134
+ upsert(row: ProjectRowInput<T>, options?: {
135
+ onConflict?: string;
136
+ }): LuxProjectInsertBuilder<unknown>;
137
+ upsert(rows: Array<ProjectRowInput<T>>, options?: {
138
+ onConflict?: string;
139
+ }): LuxProjectInsertBuilder<unknown[]>;
134
140
  update(patch: ProjectRowInput<T>): LuxProjectMutationBuilder<unknown>;
135
141
  delete(): LuxProjectMutationBuilder<unknown>;
136
142
  count(): Promise<LuxResult<number>>;
@@ -160,6 +166,8 @@ declare abstract class LuxProjectFilterBuilder<TResult, TSelf> extends LuxProjec
160
166
  lt(column: string, value: QueryValue): TSelf;
161
167
  lte(column: string, value: QueryValue): TSelf;
162
168
  is(column: string, value: QueryValue): TSelf;
169
+ isNull(column: string): TSelf;
170
+ isNotNull(column: string): TSelf;
163
171
  in(column: string, values: QueryValue[]): TSelf;
164
172
  notIn(column: string, values: QueryValue[]): TSelf;
165
173
  isValid(column: string): TSelf;
@@ -225,7 +233,11 @@ export declare class LuxProjectInsertBuilder<TResult> extends LuxProjectThenable
225
233
  private client;
226
234
  private tableName;
227
235
  private rowOrRows;
228
- constructor(client: LuxProjectClient, tableName: string, rowOrRows: Record<string, QueryValue> | Array<Record<string, QueryValue>>);
236
+ private upsertOptions?;
237
+ constructor(client: LuxProjectClient, tableName: string, rowOrRows: Record<string, QueryValue> | Array<Record<string, QueryValue>>, upsertOptions?: {
238
+ upsert: boolean;
239
+ onConflict?: string;
240
+ } | undefined);
229
241
  execute(): Promise<LuxResult<TResult>>;
230
242
  }
231
243
  export declare class LuxProjectMutationBuilder<TResult> extends LuxProjectFilterBuilder<TResult, LuxProjectMutationBuilder<TResult>> {
@@ -17,6 +17,11 @@ export interface LuxServerClientOptions extends Omit<LuxProjectOptions, 'url' |
17
17
  auth?: Omit<NonNullable<LuxProjectOptions['auth']>, 'storage'> & {
18
18
  cookieOptions?: LuxCookieOptions;
19
19
  };
20
- cookies: LuxCookieMethods;
20
+ /**
21
+ * Cookie adapter for SSR session persistence (Next/SvelteKit/etc). Omit it
22
+ * for a stateless backend client (secret key, or `setSession` per request):
23
+ * `createServerClient(url, key)` then works with no cookie plumbing.
24
+ */
25
+ cookies?: LuxCookieMethods;
21
26
  }
22
- export declare function createServerClient(url: string, key: string, options: LuxServerClientOptions): import("./project").LuxProjectClient;
27
+ export declare function createServerClient(url: string, key: string, options?: LuxServerClientOptions): import("./project").LuxProjectClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxdb/sdk",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Official Lux TypeScript SDK for app data, auth, tables, vectors, realtime, and Redis-compatible direct access",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",