@ragable/sdk 0.8.2 → 0.9.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.js CHANGED
@@ -24,6 +24,12 @@ var index_exports = {};
24
24
  __export(index_exports, {
25
25
  AuthBroadcastChannel: () => AuthBroadcastChannel,
26
26
  BrowserStorageBucketClient: () => BrowserStorageBucketClient,
27
+ CollectionDeleteBuilder: () => CollectionDeleteBuilder,
28
+ CollectionInsertChain: () => CollectionInsertChain,
29
+ CollectionMutationReturning: () => CollectionMutationReturning,
30
+ CollectionSelectBuilder: () => CollectionSelectBuilder,
31
+ CollectionUpdateBuilder: () => CollectionUpdateBuilder,
32
+ CollectionUpsertBuilder: () => CollectionUpsertBuilder,
27
33
  CookieStorageAdapter: () => CookieStorageAdapter,
28
34
  DEFAULT_RAGABLE_API_BASE: () => DEFAULT_RAGABLE_API_BASE,
29
35
  LocalStorageAdapter: () => LocalStorageAdapter,
@@ -84,6 +90,7 @@ __export(index_exports, {
84
90
  normalizeBrowserApiBase: () => normalizeBrowserApiBase,
85
91
  parseAgentStreamAgentInfo: () => parseAgentStreamAgentInfo,
86
92
  parseAgentStreamDone: () => parseAgentStreamDone,
93
+ parseOrString: () => parseOrString,
87
94
  parseSseDataLine: () => parseSseDataLine,
88
95
  parseTransportResponse: () => parseTransportResponse,
89
96
  readSseStream: () => readSseStream,
@@ -1845,6 +1852,657 @@ var PostgrestTableApi = class {
1845
1852
  }
1846
1853
  };
1847
1854
 
1855
+ // src/collection-query.ts
1856
+ function flattenRecord(record) {
1857
+ return {
1858
+ ...record.data,
1859
+ id: record.id,
1860
+ createdAt: record.createdAt,
1861
+ updatedAt: record.updatedAt
1862
+ };
1863
+ }
1864
+ function parseColumns(columns) {
1865
+ const trimmed = (columns ?? "*").trim();
1866
+ if (!trimmed || trimmed === "*") return null;
1867
+ const fields = trimmed.split(",").map((c) => c.trim()).filter(Boolean).filter((c) => c !== "*");
1868
+ return fields.length > 0 ? fields : null;
1869
+ }
1870
+ function projectRow(row, fields) {
1871
+ if (!fields) return row;
1872
+ const out = {
1873
+ id: row.id,
1874
+ createdAt: row.createdAt,
1875
+ updatedAt: row.updatedAt
1876
+ };
1877
+ for (const field of fields) {
1878
+ if (field in row) out[field] = row[field];
1879
+ }
1880
+ return out;
1881
+ }
1882
+ var FILTER_OPS = /* @__PURE__ */ new Set([
1883
+ "eq",
1884
+ "neq",
1885
+ "gt",
1886
+ "gte",
1887
+ "lt",
1888
+ "lte",
1889
+ "like",
1890
+ "ilike",
1891
+ "startsWith",
1892
+ "endsWith",
1893
+ "in",
1894
+ "nin",
1895
+ "contains",
1896
+ "is",
1897
+ "exists"
1898
+ ]);
1899
+ function splitTopLevel(input) {
1900
+ const parts = [];
1901
+ let depth = 0;
1902
+ let inQuotes = false;
1903
+ let current = "";
1904
+ for (const ch of input) {
1905
+ if (ch === '"') inQuotes = !inQuotes;
1906
+ if (!inQuotes) {
1907
+ if (ch === "(") depth++;
1908
+ else if (ch === ")") depth = Math.max(0, depth - 1);
1909
+ if (ch === "," && depth === 0) {
1910
+ parts.push(current);
1911
+ current = "";
1912
+ continue;
1913
+ }
1914
+ }
1915
+ current += ch;
1916
+ }
1917
+ parts.push(current);
1918
+ return parts.map((p) => p.trim()).filter(Boolean);
1919
+ }
1920
+ function coerceOrValue(raw) {
1921
+ const v = raw.trim();
1922
+ if (v === "true") return true;
1923
+ if (v === "false") return false;
1924
+ if (v === "null") return null;
1925
+ if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
1926
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) return v.slice(1, -1);
1927
+ return v;
1928
+ }
1929
+ function orSyntaxError(segment) {
1930
+ return new RagableError(
1931
+ `.or(): could not parse "${segment}". Expected "field.op.value" segments separated by commas, e.g. .or('done.eq.true,priority.gte.3') or .or('and(a.eq.1,b.eq.2),c.is.null'). Ops: ${[...FILTER_OPS].join(", ")} (prefix with "not." to negate).`,
1932
+ 400,
1933
+ { code: "SDK_COLLECTION_OR_SYNTAX" }
1934
+ );
1935
+ }
1936
+ function mergeCondition(group, field, op, value, negate) {
1937
+ const condition = negate ? { not: { [op]: value } } : { [op]: value };
1938
+ const existing = group[field];
1939
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
1940
+ const prev = existing;
1941
+ if (negate && prev.not && typeof prev.not === "object" && !Array.isArray(prev.not)) {
1942
+ group[field] = {
1943
+ ...prev,
1944
+ not: { ...prev.not, [op]: value }
1945
+ };
1946
+ return;
1947
+ }
1948
+ group[field] = { ...prev, ...condition };
1949
+ return;
1950
+ }
1951
+ group[field] = condition;
1952
+ }
1953
+ function parseOrCondition(segment, group) {
1954
+ const firstDot = segment.indexOf(".");
1955
+ if (firstDot <= 0) throw orSyntaxError(segment);
1956
+ const field = segment.slice(0, firstDot);
1957
+ let rest = segment.slice(firstDot + 1);
1958
+ let negate = false;
1959
+ if (rest.startsWith("not.")) {
1960
+ negate = true;
1961
+ rest = rest.slice(4);
1962
+ }
1963
+ const opDot = rest.indexOf(".");
1964
+ if (opDot <= 0) throw orSyntaxError(segment);
1965
+ const op = rest.slice(0, opDot);
1966
+ const rawValue = rest.slice(opDot + 1);
1967
+ if (!FILTER_OPS.has(op)) throw orSyntaxError(segment);
1968
+ let value;
1969
+ if (op === "in" || op === "nin") {
1970
+ const inner = rawValue.replace(/^\(/, "").replace(/\)$/, "");
1971
+ value = inner.trim().length === 0 ? [] : inner.split(",").map(coerceOrValue);
1972
+ } else {
1973
+ value = coerceOrValue(rawValue);
1974
+ }
1975
+ mergeCondition(group, field, op, value, negate);
1976
+ }
1977
+ function parseOrString(input) {
1978
+ const groups = [];
1979
+ for (const segment of splitTopLevel(input)) {
1980
+ if (/^and\(/.test(segment) && segment.endsWith(")")) {
1981
+ const inner = segment.slice(segment.indexOf("(") + 1, -1);
1982
+ const group = {};
1983
+ for (const condition of splitTopLevel(inner)) parseOrCondition(condition, group);
1984
+ if (Object.keys(group).length === 0) throw orSyntaxError(segment);
1985
+ groups.push(group);
1986
+ } else {
1987
+ const group = {};
1988
+ parseOrCondition(segment, group);
1989
+ groups.push(group);
1990
+ }
1991
+ }
1992
+ if (groups.length === 0) throw orSyntaxError(input);
1993
+ return groups;
1994
+ }
1995
+ var CollectionConditionBuilder = class {
1996
+ constructor() {
1997
+ __publicField(this, "filters", []);
1998
+ __publicField(this, "orGroups", null);
1999
+ __publicField(this, "signal");
2000
+ }
2001
+ /** `field = value` (or `IS NULL` when value is null). */
2002
+ eq(field, value) {
2003
+ return this.push(field, "eq", value);
2004
+ }
2005
+ /** `field != value` (null-aware). */
2006
+ neq(field, value) {
2007
+ return this.push(field, "neq", value);
2008
+ }
2009
+ gt(field, value) {
2010
+ return this.push(field, "gt", value);
2011
+ }
2012
+ gte(field, value) {
2013
+ return this.push(field, "gte", value);
2014
+ }
2015
+ lt(field, value) {
2016
+ return this.push(field, "lt", value);
2017
+ }
2018
+ lte(field, value) {
2019
+ return this.push(field, "lte", value);
2020
+ }
2021
+ /** SQL LIKE — you supply the `%` wildcards, e.g. `.like("title", "%report%")`. */
2022
+ like(field, pattern) {
2023
+ return this.push(field, "like", pattern);
2024
+ }
2025
+ /** Case-insensitive LIKE. */
2026
+ ilike(field, pattern) {
2027
+ return this.push(field, "ilike", pattern);
2028
+ }
2029
+ /** Prefix match; wildcard characters in `value` are matched literally. */
2030
+ startsWith(field, value) {
2031
+ return this.push(field, "startsWith", value);
2032
+ }
2033
+ /** Suffix match; wildcard characters in `value` are matched literally. */
2034
+ endsWith(field, value) {
2035
+ return this.push(field, "endsWith", value);
2036
+ }
2037
+ /** Value is one of `values`. */
2038
+ in(field, values) {
2039
+ return this.push(field, "in", values);
2040
+ }
2041
+ /** Value is NOT one of `values`. */
2042
+ nin(field, values) {
2043
+ return this.push(field, "nin", values);
2044
+ }
2045
+ /**
2046
+ * JSONB containment: array fields contain the given element(s), object
2047
+ * fields contain the given key/value pairs.
2048
+ * `.contains("tags", ["urgent"])`, `.contains("meta", { plan: "pro" })`.
2049
+ */
2050
+ contains(field, value) {
2051
+ return this.push(field, "contains", value);
2052
+ }
2053
+ /** Strict null / boolean check: `.is("archivedAt", null)`, `.is("done", true)`. */
2054
+ is(field, value) {
2055
+ return this.push(field, "is", value);
2056
+ }
2057
+ /** Whether the JSON key is present at all: `.exists("avatarUrl", false)`. */
2058
+ exists(field, present = true) {
2059
+ return this.push(field, "exists", present);
2060
+ }
2061
+ /** Negated condition: `.not("status", "eq", "archived")`. */
2062
+ not(field, op, value) {
2063
+ this.assertOp(op);
2064
+ this.filters.push({ field: String(field), op, value, not: true });
2065
+ return this;
2066
+ }
2067
+ /** Generic escape hatch, mirroring Supabase `.filter(column, op, value)`. */
2068
+ filter(field, op, value) {
2069
+ this.assertOp(op);
2070
+ return this.push(field, op, value);
2071
+ }
2072
+ /** Equality on every key of `query` (null values become IS NULL). */
2073
+ match(query) {
2074
+ for (const [field, value] of Object.entries(query)) {
2075
+ this.push(field, "eq", value);
2076
+ }
2077
+ return this;
2078
+ }
2079
+ /**
2080
+ * OR conditions, ANDed with the other filters. Accepts the Supabase string
2081
+ * grammar — `.or('done.eq.true,priority.gte.3')`, with `and(...)` for a
2082
+ * multi-condition branch — or an array of where-style objects:
2083
+ * `.or([{ done: true }, { priority: { gte: 3 } }])`.
2084
+ *
2085
+ * One `.or()` per query: the wire format carries a single disjunction.
2086
+ */
2087
+ or(conditions) {
2088
+ if (this.orGroups) {
2089
+ throw new RagableError(
2090
+ ".or() can only be used once per query. Combine branches in a single call: .or('a.eq.1,b.eq.2').",
2091
+ 400,
2092
+ { code: "SDK_COLLECTION_OR_TWICE" }
2093
+ );
2094
+ }
2095
+ this.orGroups = typeof conditions === "string" ? parseOrString(conditions) : conditions;
2096
+ if (!Array.isArray(this.orGroups) || this.orGroups.length === 0) {
2097
+ throw new RagableError(
2098
+ ".or() requires at least one condition.",
2099
+ 400,
2100
+ { code: "SDK_COLLECTION_OR_EMPTY" }
2101
+ );
2102
+ }
2103
+ return this;
2104
+ }
2105
+ abortSignal(signal) {
2106
+ this.signal = signal;
2107
+ return this;
2108
+ }
2109
+ get hasConditions() {
2110
+ return this.filters.length > 0 || (this.orGroups?.length ?? 0) > 0;
2111
+ }
2112
+ wireFilters() {
2113
+ return this.filters.map((f) => ({
2114
+ field: f.field,
2115
+ op: f.op,
2116
+ value: f.value,
2117
+ ...f.not ? { not: true } : {}
2118
+ }));
2119
+ }
2120
+ assertOp(op) {
2121
+ if (!FILTER_OPS.has(op)) {
2122
+ throw new RagableError(
2123
+ `Unknown filter operator "${op}". Use one of: ${[...FILTER_OPS].join(", ")}.`,
2124
+ 400,
2125
+ { code: "SDK_COLLECTION_BAD_OP" }
2126
+ );
2127
+ }
2128
+ }
2129
+ push(field, op, value) {
2130
+ this.filters.push({ field: String(field), op, value });
2131
+ return this;
2132
+ }
2133
+ };
2134
+ var CollectionSelectBuilder = class extends CollectionConditionBuilder {
2135
+ constructor(request, columns, options) {
2136
+ super();
2137
+ this.request = request;
2138
+ __publicField(this, "_limit");
2139
+ __publicField(this, "_offset");
2140
+ __publicField(this, "_order", []);
2141
+ __publicField(this, "columns");
2142
+ __publicField(this, "wantCount");
2143
+ __publicField(this, "headOnly");
2144
+ this.columns = parseColumns(columns);
2145
+ this.wantCount = options?.count === "exact";
2146
+ this.headOnly = options?.head === true;
2147
+ }
2148
+ /** Sort by a field. Call again to add secondary sort keys (max 4). */
2149
+ order(field, options) {
2150
+ this._order.push({
2151
+ field: String(field),
2152
+ direction: options?.ascending === true ? "asc" : "desc"
2153
+ });
2154
+ return this;
2155
+ }
2156
+ limit(n) {
2157
+ this._limit = n;
2158
+ return this;
2159
+ }
2160
+ offset(n) {
2161
+ this._offset = n;
2162
+ return this;
2163
+ }
2164
+ /** Rows `from`..`to` inclusive (zero-based), like Supabase `.range()`. */
2165
+ range(from, to) {
2166
+ this._offset = from;
2167
+ this._limit = Math.max(0, to - from + 1);
2168
+ return this;
2169
+ }
2170
+ then(onfulfilled, onrejected) {
2171
+ return this.execute().then(onfulfilled, onrejected);
2172
+ }
2173
+ /**
2174
+ * Exactly one row. 0 or >1 matching rows is an error (code PGRST116),
2175
+ * mirroring Supabase `.single()`.
2176
+ */
2177
+ async single() {
2178
+ const res = await this.executeRows(2);
2179
+ if (res.error) return { data: null, error: res.error };
2180
+ const rows = res.rows;
2181
+ if (rows.length !== 1) {
2182
+ return {
2183
+ data: null,
2184
+ error: new RagableError(
2185
+ "JSON object requested, multiple (or no) rows returned",
2186
+ 406,
2187
+ { code: "PGRST116", details: `The result contains ${rows.length} rows` }
2188
+ )
2189
+ };
2190
+ }
2191
+ return { data: rows[0], error: null };
2192
+ }
2193
+ /** One row or `null` — only >1 matching rows is an error. */
2194
+ async maybeSingle() {
2195
+ const res = await this.executeRows(2);
2196
+ if (res.error) return { data: null, error: res.error };
2197
+ const rows = res.rows;
2198
+ if (rows.length > 1) {
2199
+ return {
2200
+ data: null,
2201
+ error: new RagableError(
2202
+ "JSON object requested, multiple (or no) rows returned",
2203
+ 406,
2204
+ { code: "PGRST116", details: `The result contains ${rows.length} rows` }
2205
+ )
2206
+ };
2207
+ }
2208
+ return { data: rows[0] ?? null, error: null };
2209
+ }
2210
+ buildBody(limitOverride) {
2211
+ const body = {};
2212
+ const filters = this.wireFilters();
2213
+ if (filters.length > 0) body.filters = filters;
2214
+ if (this.orGroups) body.or = this.orGroups;
2215
+ const limit = limitOverride ?? (this.headOnly ? 0 : this._limit);
2216
+ if (limit !== void 0) body.limit = limit;
2217
+ if (this._offset !== void 0 && this._offset > 0) body.offset = this._offset;
2218
+ if (this._order.length === 1) {
2219
+ body.orderBy = this._order[0].field;
2220
+ body.orderDirection = this._order[0].direction;
2221
+ } else if (this._order.length > 1) {
2222
+ body.orderBy = this._order.map((o) => ({
2223
+ field: o.field,
2224
+ direction: o.direction
2225
+ }));
2226
+ }
2227
+ if (this.wantCount) body.count = true;
2228
+ return body;
2229
+ }
2230
+ async executeRows(limitOverride) {
2231
+ const result = await asPostgrestResponse(
2232
+ () => this.request(
2233
+ "POST",
2234
+ "/find",
2235
+ this.buildBody(limitOverride),
2236
+ this.signal
2237
+ )
2238
+ );
2239
+ if (result.error) return { error: result.error, rows: null, total: null };
2240
+ const payload = result.data;
2241
+ const records = Array.isArray(payload) ? payload : payload?.records ?? [];
2242
+ const total = Array.isArray(payload) ? null : payload?.total ?? null;
2243
+ const rows = records.map((r) => projectRow(flattenRecord(r), this.columns));
2244
+ return { error: null, rows, total };
2245
+ }
2246
+ async execute() {
2247
+ const res = await this.executeRows();
2248
+ if (res.error) return { data: null, error: res.error, count: null };
2249
+ return {
2250
+ data: this.headOnly ? [] : res.rows,
2251
+ error: null,
2252
+ count: res.total
2253
+ };
2254
+ }
2255
+ };
2256
+ var CollectionMutationReturning = class {
2257
+ constructor(run, columns) {
2258
+ this.run = run;
2259
+ this.columns = columns;
2260
+ }
2261
+ then(onfulfilled, onrejected) {
2262
+ return this.executeMany().then(onfulfilled, onrejected);
2263
+ }
2264
+ async executeMany() {
2265
+ const res = await this.run();
2266
+ if (res.error) return { data: null, error: res.error };
2267
+ const rows = (res.data ?? []).map(
2268
+ (r) => projectRow(flattenRecord(r), this.columns)
2269
+ );
2270
+ return { data: rows, error: null };
2271
+ }
2272
+ async single() {
2273
+ const many = await this.executeMany();
2274
+ if (many.error) return { data: null, error: many.error };
2275
+ const rows = many.data ?? [];
2276
+ if (rows.length !== 1) {
2277
+ return {
2278
+ data: null,
2279
+ error: new RagableError(
2280
+ "JSON object requested, multiple (or no) rows returned",
2281
+ 406,
2282
+ { code: "PGRST116", details: `The result contains ${rows.length} rows` }
2283
+ )
2284
+ };
2285
+ }
2286
+ return { data: rows[0], error: null };
2287
+ }
2288
+ async maybeSingle() {
2289
+ const many = await this.executeMany();
2290
+ if (many.error) return { data: null, error: many.error };
2291
+ const rows = many.data ?? [];
2292
+ if (rows.length > 1) {
2293
+ return {
2294
+ data: null,
2295
+ error: new RagableError(
2296
+ "JSON object requested, multiple (or no) rows returned",
2297
+ 406,
2298
+ { code: "PGRST116", details: `The result contains ${rows.length} rows` }
2299
+ )
2300
+ };
2301
+ }
2302
+ return { data: rows[0] ?? null, error: null };
2303
+ }
2304
+ };
2305
+ var CollectionUpdateBuilder = class extends CollectionConditionBuilder {
2306
+ constructor(request, patch) {
2307
+ super();
2308
+ this.request = request;
2309
+ this.patch = patch;
2310
+ __publicField(this, "_limit");
2311
+ }
2312
+ limit(n) {
2313
+ this._limit = n;
2314
+ return this;
2315
+ }
2316
+ select(columns = "*") {
2317
+ return new CollectionMutationReturning(
2318
+ () => this.execute(),
2319
+ parseColumns(columns)
2320
+ );
2321
+ }
2322
+ then(onfulfilled, onrejected) {
2323
+ return this.execute().then(
2324
+ (res) => res.error ? { data: null, error: res.error } : { data: null, error: null }
2325
+ ).then(onfulfilled, onrejected);
2326
+ }
2327
+ async execute() {
2328
+ if (!this.hasConditions) {
2329
+ return {
2330
+ data: null,
2331
+ error: new RagableError(
2332
+ "update() requires at least one filter \u2014 add .eq()/.match()/.or() before awaiting. To update every record, filter on a condition that always holds, e.g. .neq('id', null).",
2333
+ 400,
2334
+ { code: "SDK_COLLECTION_UPDATE_NO_FILTER" }
2335
+ )
2336
+ };
2337
+ }
2338
+ return asPostgrestResponse(
2339
+ () => this.request(
2340
+ "PATCH",
2341
+ "/records",
2342
+ {
2343
+ where: {},
2344
+ filters: this.wireFilters(),
2345
+ ...this.orGroups ? { or: this.orGroups } : {},
2346
+ patch: this.patch,
2347
+ ...this._limit !== void 0 ? { limit: this._limit } : {}
2348
+ },
2349
+ this.signal
2350
+ )
2351
+ );
2352
+ }
2353
+ };
2354
+ var CollectionDeleteBuilder = class extends CollectionConditionBuilder {
2355
+ constructor(request) {
2356
+ super();
2357
+ this.request = request;
2358
+ __publicField(this, "_limit");
2359
+ }
2360
+ limit(n) {
2361
+ this._limit = n;
2362
+ return this;
2363
+ }
2364
+ select(columns = "*") {
2365
+ return new CollectionMutationReturning(
2366
+ () => this.execute(),
2367
+ parseColumns(columns)
2368
+ );
2369
+ }
2370
+ then(onfulfilled, onrejected) {
2371
+ return this.execute().then(
2372
+ (res) => res.error ? { data: null, error: res.error } : { data: null, error: null }
2373
+ ).then(onfulfilled, onrejected);
2374
+ }
2375
+ async execute() {
2376
+ if (!this.hasConditions) {
2377
+ return {
2378
+ data: null,
2379
+ error: new RagableError(
2380
+ "delete() requires at least one filter \u2014 add .eq()/.match()/.or() before awaiting. To clear a collection, filter on a condition that always holds, e.g. .neq('id', null).",
2381
+ 400,
2382
+ { code: "SDK_COLLECTION_DELETE_NO_FILTER" }
2383
+ )
2384
+ };
2385
+ }
2386
+ const res = await asPostgrestResponse(
2387
+ () => this.request(
2388
+ "DELETE",
2389
+ "/records",
2390
+ {
2391
+ where: {},
2392
+ filters: this.wireFilters(),
2393
+ ...this.orGroups ? { or: this.orGroups } : {},
2394
+ ...this._limit !== void 0 ? { limit: this._limit } : {}
2395
+ },
2396
+ this.signal
2397
+ )
2398
+ );
2399
+ if (res.error) return { data: null, error: res.error };
2400
+ return { data: res.data.records ?? [], error: null };
2401
+ }
2402
+ };
2403
+ var CollectionInsertChain = class {
2404
+ constructor(request, rows, single) {
2405
+ this.request = request;
2406
+ this.rows = rows;
2407
+ this.single = single;
2408
+ __publicField(this, "_signal");
2409
+ }
2410
+ abortSignal(signal) {
2411
+ this._signal = signal;
2412
+ return this;
2413
+ }
2414
+ select(columns = "*") {
2415
+ return new CollectionMutationReturning(
2416
+ async () => {
2417
+ const res = await this.executeEnvelopes();
2418
+ if (res.error) return { data: null, error: res.error };
2419
+ return { data: res.data, error: null };
2420
+ },
2421
+ parseColumns(columns)
2422
+ );
2423
+ }
2424
+ then(onfulfilled, onrejected) {
2425
+ return this.executeEnvelopes().then((res) => {
2426
+ if (res.error) return { data: null, error: res.error };
2427
+ const envelopes = res.data;
2428
+ const resolved = this.single ? envelopes[0] ?? null : envelopes;
2429
+ return { data: resolved, error: null };
2430
+ }).then(onfulfilled, onrejected);
2431
+ }
2432
+ async executeEnvelopes() {
2433
+ if (this.rows.length === 0) return { data: [], error: null };
2434
+ if (this.single) {
2435
+ const res = await asPostgrestResponse(
2436
+ () => this.request(
2437
+ "POST",
2438
+ "/records",
2439
+ { data: this.rows[0] },
2440
+ this._signal
2441
+ )
2442
+ );
2443
+ if (res.error) return { data: null, error: res.error };
2444
+ return { data: [res.data], error: null };
2445
+ }
2446
+ return asPostgrestResponse(
2447
+ () => this.request(
2448
+ "POST",
2449
+ "/records/batch",
2450
+ { items: this.rows },
2451
+ this._signal
2452
+ )
2453
+ );
2454
+ }
2455
+ };
2456
+ var CollectionUpsertBuilder = class {
2457
+ constructor(request, rows, options) {
2458
+ this.request = request;
2459
+ this.rows = rows;
2460
+ this.options = options;
2461
+ __publicField(this, "_signal");
2462
+ }
2463
+ abortSignal(signal) {
2464
+ this._signal = signal;
2465
+ return this;
2466
+ }
2467
+ select(columns = "*") {
2468
+ return new CollectionMutationReturning(
2469
+ async () => {
2470
+ const res = await this.execute();
2471
+ if (res.error) return { data: null, error: res.error };
2472
+ return { data: res.data.records, error: null };
2473
+ },
2474
+ parseColumns(columns)
2475
+ );
2476
+ }
2477
+ then(onfulfilled, onrejected) {
2478
+ return this.execute().then((res) => {
2479
+ if (res.error) return { data: null, error: res.error };
2480
+ const { inserted, updated, skipped } = res.data;
2481
+ return { data: { inserted, updated, skipped }, error: null };
2482
+ }).then(onfulfilled, onrejected);
2483
+ }
2484
+ async execute() {
2485
+ if (this.rows.length === 0) {
2486
+ return {
2487
+ data: { records: [], inserted: 0, updated: 0, skipped: 0 },
2488
+ error: null
2489
+ };
2490
+ }
2491
+ return asPostgrestResponse(
2492
+ () => this.request(
2493
+ "POST",
2494
+ "/records/upsert",
2495
+ {
2496
+ items: this.rows,
2497
+ onConflict: this.options.onConflict ?? "id",
2498
+ ...this.options.ignoreDuplicates ? { ignoreDuplicates: true } : {}
2499
+ },
2500
+ this._signal
2501
+ )
2502
+ );
2503
+ }
2504
+ };
2505
+
1848
2506
  // src/auth-storage.ts
1849
2507
  var LocalStorageAdapter = class {
1850
2508
  getItem(key) {
@@ -2139,6 +2797,109 @@ var RagableAuth = class {
2139
2797
  this.emit("SIGNED_OUT", null);
2140
2798
  return { error: null };
2141
2799
  }
2800
+ // ── Password recovery & magic links ────────────────────────────────────────
2801
+ /**
2802
+ * Email a password-recovery link. The link points at `redirectTo` (or the
2803
+ * site's deployed domain when omitted) with `?token_hash=…&type=recovery`
2804
+ * appended. On that page, either:
2805
+ * - call {@link resetPassword} with the token and the new password, or
2806
+ * - call {@link verifyOtp} to sign the user in, then `updateUser({ password })`.
2807
+ *
2808
+ * Always resolves successfully for well-formed requests — whether the email
2809
+ * has an account is never revealed.
2810
+ */
2811
+ async resetPasswordForEmail(email, options) {
2812
+ return asPostgrestResponse(async () => {
2813
+ await this.fetchAuth("/forgot-password", "POST", {
2814
+ email,
2815
+ ...options?.redirectTo ? { redirectTo: options.redirectTo } : {}
2816
+ });
2817
+ return {};
2818
+ });
2819
+ }
2820
+ /**
2821
+ * Email a one-click sign-in (magic) link — passwordless auth. New addresses
2822
+ * get an account automatically unless `shouldCreateUser: false`. The emailed
2823
+ * link carries `?token_hash=…&type=magiclink`; complete sign-in on that page
2824
+ * with {@link verifyOtp}.
2825
+ */
2826
+ async signInWithOtp(params) {
2827
+ return asPostgrestResponse(async () => {
2828
+ await this.fetchAuth("/magic-link", "POST", {
2829
+ email: params.email,
2830
+ ...params.options?.emailRedirectTo ? { redirectTo: params.options.emailRedirectTo } : {},
2831
+ ...params.options?.shouldCreateUser === false ? { createUser: false } : {}
2832
+ });
2833
+ return { user: null, session: null };
2834
+ });
2835
+ }
2836
+ /**
2837
+ * Exchange an emailed `token_hash` for a session. Reads the token from the
2838
+ * URL your recovery / magic-link page receives:
2839
+ *
2840
+ * ```ts
2841
+ * const qs = new URLSearchParams(window.location.search);
2842
+ * const { data, error } = await client.auth.verifyOtp({
2843
+ * token_hash: qs.get("token_hash")!,
2844
+ * type: qs.get("type") as "recovery" | "magiclink",
2845
+ * });
2846
+ * ```
2847
+ *
2848
+ * Emits `SIGNED_IN` (and `PASSWORD_RECOVERY` for recovery tokens — listen
2849
+ * for it to route to your "choose a new password" screen).
2850
+ */
2851
+ async verifyOtp(params) {
2852
+ return asPostgrestResponse(async () => {
2853
+ const token = (params.token_hash ?? params.tokenHash ?? "").trim();
2854
+ if (!token) {
2855
+ throw new RagableError(
2856
+ "verifyOtp requires the token_hash from the emailed link.",
2857
+ 400,
2858
+ { code: "SDK_AUTH_MISSING_TOKEN_HASH" }
2859
+ );
2860
+ }
2861
+ const type = params.type === "email" ? "magiclink" : params.type;
2862
+ const raw = await this.fetchAuth("/verify-token", "POST", { token, type });
2863
+ const session = this.rawToSession(raw);
2864
+ await this.setSessionInternal(session, "SIGNED_IN");
2865
+ if (type === "recovery") this.emit("PASSWORD_RECOVERY", session);
2866
+ return { user: session.user, session };
2867
+ });
2868
+ }
2869
+ /**
2870
+ * One-shot recovery: verify the emailed recovery token, set the new
2871
+ * password, and sign the user in — no intermediate session juggling.
2872
+ *
2873
+ * ```ts
2874
+ * const qs = new URLSearchParams(window.location.search);
2875
+ * const { data, error } = await client.auth.resetPassword({
2876
+ * tokenHash: qs.get("token_hash")!,
2877
+ * newPassword: form.password,
2878
+ * });
2879
+ * ```
2880
+ *
2881
+ * Recovery links are single-use: once the password changes, the same link
2882
+ * is rejected.
2883
+ */
2884
+ async resetPassword(params) {
2885
+ return asPostgrestResponse(async () => {
2886
+ const token = (params.tokenHash ?? params.token_hash ?? "").trim();
2887
+ if (!token) {
2888
+ throw new RagableError(
2889
+ "resetPassword requires the token_hash from the recovery email link.",
2890
+ 400,
2891
+ { code: "SDK_AUTH_MISSING_TOKEN_HASH" }
2892
+ );
2893
+ }
2894
+ const raw = await this.fetchAuth("/reset-password", "POST", {
2895
+ token,
2896
+ password: params.newPassword
2897
+ });
2898
+ const session = this.rawToSession(raw);
2899
+ await this.setSessionInternal(session, "SIGNED_IN");
2900
+ return { user: session.user, session };
2901
+ });
2902
+ }
2142
2903
  async refreshSession(refreshToken) {
2143
2904
  return asPostgrestResponse(async () => {
2144
2905
  const token = refreshToken ?? this.currentSession?.refresh_token;
@@ -3350,6 +4111,22 @@ var RagableBrowserAuthClient = class {
3350
4111
  async signOut(_options) {
3351
4112
  return this.auth.signOut(_options);
3352
4113
  }
4114
+ /** Email a password-recovery link — see {@link RagableAuth.resetPasswordForEmail}. */
4115
+ async resetPasswordForEmail(email, options) {
4116
+ return this.auth.resetPasswordForEmail(email, options);
4117
+ }
4118
+ /** Email a magic sign-in link — see {@link RagableAuth.signInWithOtp}. */
4119
+ async signInWithOtp(params) {
4120
+ return this.auth.signInWithOtp(params);
4121
+ }
4122
+ /** Exchange an emailed token_hash for a session — see {@link RagableAuth.verifyOtp}. */
4123
+ async verifyOtp(params) {
4124
+ return this.auth.verifyOtp(params);
4125
+ }
4126
+ /** Verify a recovery token and set a new password in one call — see {@link RagableAuth.resetPassword}. */
4127
+ async resetPassword(params) {
4128
+ return this.auth.resetPassword(params);
4129
+ }
3353
4130
  async register(body) {
3354
4131
  return this.auth.register(body);
3355
4132
  }
@@ -3401,6 +4178,39 @@ var BrowserCollectionApi = class {
3401
4178
  this.database = database;
3402
4179
  this.name = name;
3403
4180
  this.databaseInstanceId = databaseInstanceId;
4181
+ /** Transport for the chainable builders, bound to this collection. */
4182
+ __publicField(this, "chainRequest", (method, path, body, signal) => this.database._requestCollection(
4183
+ method,
4184
+ `/${encodeURIComponent(this.name)}${path}`,
4185
+ body,
4186
+ this.databaseInstanceId,
4187
+ signal
4188
+ ));
4189
+ /**
4190
+ * Supabase-style chainable query. Rows come back flat (`id`, `createdAt`,
4191
+ * `updatedAt` merged into the document fields).
4192
+ *
4193
+ * ```ts
4194
+ * const { data, error, count } = await collections.todos
4195
+ * .select("*", { count: "exact" })
4196
+ * .eq("done", false)
4197
+ * .or("priority.gte.3,starred.eq.true")
4198
+ * .order("createdAt", { ascending: false })
4199
+ * .range(0, 19);
4200
+ * ```
4201
+ */
4202
+ __publicField(this, "select", (columns, options) => new CollectionSelectBuilder(this.chainRequest, columns, options));
4203
+ /**
4204
+ * Insert-or-update matched on `onConflict` (`"id"` by default, or any data
4205
+ * field, e.g. `{ onConflict: "slug" }`). Chain `.select()` for the affected
4206
+ * rows. Match-based (no unique indexes): concurrent upserts of the same new
4207
+ * value can both insert.
4208
+ */
4209
+ __publicField(this, "upsert", (values, options) => new CollectionUpsertBuilder(
4210
+ this.chainRequest,
4211
+ Array.isArray(values) ? values : [values],
4212
+ options ?? {}
4213
+ ));
3404
4214
  __publicField(this, "requestFind", (body) => asPostgrestResponse(
3405
4215
  () => this.database._requestCollection(
3406
4216
  "POST",
@@ -3444,14 +4254,20 @@ var BrowserCollectionApi = class {
3444
4254
  __publicField(this, "findUnique", async (args) => {
3445
4255
  return this.findFirst({ where: args.where });
3446
4256
  });
3447
- __publicField(this, "insert", (data) => asPostgrestResponse(
3448
- () => this.database._requestCollection(
3449
- "POST",
3450
- `/${encodeURIComponent(this.name)}/records`,
3451
- { data },
3452
- this.databaseInstanceId
3453
- )
3454
- ));
4257
+ /**
4258
+ * Insert one row or an array of rows. Chain `.select()` for the inserted
4259
+ * rows in flat shape, or await directly for the record envelope(s)
4260
+ * (back-compat extension; Supabase resolves to null without `.select()`).
4261
+ */
4262
+ __publicField(this, "insert", ((data) => {
4263
+ const isArray = Array.isArray(data);
4264
+ const rows = isArray ? data : [data];
4265
+ return new CollectionInsertChain(
4266
+ this.chainRequest,
4267
+ rows,
4268
+ !isArray
4269
+ );
4270
+ }));
3455
4271
  /**
3456
4272
  * Insert multiple rows in one request (server multi-value `INSERT`, single transaction).
3457
4273
  * Empty **`items`** resolves to an empty array. Max batch size is enforced on the server (500).
@@ -3465,16 +4281,28 @@ var BrowserCollectionApi = class {
3465
4281
  )
3466
4282
  ));
3467
4283
  /**
3468
- * Update rows matching `where` (JSON fields, plus envelope `id` / `createdAt` / `updatedAt`).
4284
+ * Two forms:
4285
+ * - `update(patch)` — Supabase-style chain: add filters, then await.
4286
+ * `update({ done: true }).eq("id", id)` (optionally `.select()`).
4287
+ * - `update(where, patch)` — legacy direct call, returns updated records.
3469
4288
  */
3470
- __publicField(this, "update", (where, patch, options) => asPostgrestResponse(
3471
- () => this.database._requestCollection(
3472
- "PATCH",
3473
- `/${encodeURIComponent(this.name)}/records`,
3474
- { where, patch, ...options?.limit ? { limit: options.limit } : {} },
3475
- this.databaseInstanceId
3476
- )
3477
- ));
4289
+ __publicField(this, "update", ((...args) => {
4290
+ if (args.length >= 2) {
4291
+ const [where, patch, options] = args;
4292
+ return asPostgrestResponse(
4293
+ () => this.database._requestCollection(
4294
+ "PATCH",
4295
+ `/${encodeURIComponent(this.name)}/records`,
4296
+ { where, patch, ...options?.limit ? { limit: options.limit } : {} },
4297
+ this.databaseInstanceId
4298
+ )
4299
+ );
4300
+ }
4301
+ return new CollectionUpdateBuilder(
4302
+ this.chainRequest,
4303
+ args[0]
4304
+ );
4305
+ }));
3478
4306
  /**
3479
4307
  * Like {@link BrowserCollectionApi.update} but the success payload includes
3480
4308
  * `meta.count` (number of rows returned from the update, bounded by `limit`).
@@ -3484,14 +4312,26 @@ var BrowserCollectionApi = class {
3484
4312
  if (r.error) return r;
3485
4313
  return { data: { records: r.data, meta: { count: r.data.length } }, error: null };
3486
4314
  });
3487
- __publicField(this, "delete", (where, options) => asPostgrestResponse(
3488
- () => this.database._requestCollection(
3489
- "DELETE",
3490
- `/${encodeURIComponent(this.name)}/records`,
3491
- { where, ...options?.limit ? { limit: options.limit } : {} },
3492
- this.databaseInstanceId
3493
- )
3494
- ));
4315
+ /**
4316
+ * Two forms:
4317
+ * - `delete()` — Supabase-style chain: add filters, then await.
4318
+ * `delete().eq("id", id)` (optionally `.select()`).
4319
+ * - `delete(where)` legacy direct call, returns `{ deleted, records }`.
4320
+ */
4321
+ __publicField(this, "delete", ((...args) => {
4322
+ if (args.length === 0) {
4323
+ return new CollectionDeleteBuilder(this.chainRequest);
4324
+ }
4325
+ const [where, options] = args;
4326
+ return asPostgrestResponse(
4327
+ () => this.database._requestCollection(
4328
+ "DELETE",
4329
+ `/${encodeURIComponent(this.name)}/records`,
4330
+ { where, ...options?.limit ? { limit: options.limit } : {} },
4331
+ this.databaseInstanceId
4332
+ )
4333
+ );
4334
+ }));
3495
4335
  /**
3496
4336
  * Like {@link BrowserCollectionApi.delete} but the success payload includes **`meta.count`**
3497
4337
  * (number of deleted rows), matching {@link BrowserCollectionApi.updateMany}.
@@ -3693,7 +4533,7 @@ var RagableBrowserDatabaseClient = class {
3693
4533
  toUrl(path) {
3694
4534
  return `${normalizeBrowserApiBase()}${path.startsWith("/") ? path : `/${path}`}`;
3695
4535
  }
3696
- async _requestCollection(method, path, body, databaseInstanceId) {
4536
+ async _requestCollection(method, path, body, databaseInstanceId, signal) {
3697
4537
  const gid = requireAuthGroupId(this.options);
3698
4538
  const token = await resolveDatabaseAuthBearer(this.options, this.ragableAuth);
3699
4539
  const id = databaseInstanceId?.trim() || this.options.databaseInstanceId?.trim();
@@ -3713,7 +4553,8 @@ var RagableBrowserDatabaseClient = class {
3713
4553
  {
3714
4554
  method,
3715
4555
  headers,
3716
- body: body !== void 0 ? JSON.stringify(body) : void 0
4556
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
4557
+ ...signal ? { signal } : {}
3717
4558
  }
3718
4559
  );
3719
4560
  const payload = await parseMaybeJsonBody(response);
@@ -4745,6 +5586,12 @@ function createClient(options) {
4745
5586
  0 && (module.exports = {
4746
5587
  AuthBroadcastChannel,
4747
5588
  BrowserStorageBucketClient,
5589
+ CollectionDeleteBuilder,
5590
+ CollectionInsertChain,
5591
+ CollectionMutationReturning,
5592
+ CollectionSelectBuilder,
5593
+ CollectionUpdateBuilder,
5594
+ CollectionUpsertBuilder,
4748
5595
  CookieStorageAdapter,
4749
5596
  DEFAULT_RAGABLE_API_BASE,
4750
5597
  LocalStorageAdapter,
@@ -4805,6 +5652,7 @@ function createClient(options) {
4805
5652
  normalizeBrowserApiBase,
4806
5653
  parseAgentStreamAgentInfo,
4807
5654
  parseAgentStreamDone,
5655
+ parseOrString,
4808
5656
  parseSseDataLine,
4809
5657
  parseTransportResponse,
4810
5658
  readSseStream,
@@ -4817,4 +5665,3 @@ function createClient(options) {
4817
5665
  unwrapPostgrest,
4818
5666
  wrapStreamTextAsObject
4819
5667
  });
4820
- //# sourceMappingURL=index.js.map