@rebasepro/client 0.19.1 → 0.19.2-canary.g09316f6
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/collection.d.ts +16 -0
- package/dist/index.d.ts +37 -2
- package/dist/index.es.js +378 -49
- package/dist/index.es.js.map +1 -1
- package/dist/offline-query.d.ts +0 -30
- package/dist/sdk_query_builder.d.ts +81 -8
- package/dist/transport.d.ts +37 -2
- package/dist/websocket.d.ts +2 -2
- package/package.json +4 -4
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, SCHEMA_VERSION_HEADER, Vector, isPublicStoragePath, isUnsupported, parseRelationAggregateSort, sortKeyToString, toCanonicalOp, unsupportedMethod } from "@rebasepro/types";
|
|
2
|
-
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, normalizeOrderBy, or, paginateFind, resolveFindWindow, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
1
|
+
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, SCHEMA_VERSION_HEADER, Vector, hasFieldOperation, isPublicStoragePath, isUnsupported, parseRelationAggregateSort, sortKeyToString, toCanonicalOp, unsupportedMethod } from "@rebasepro/types";
|
|
2
|
+
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, mergeIncludeSpecs, normalizeOrderBy, not, or, paginateFind, resolveFindWindow, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
4
4
|
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
@@ -93,6 +93,7 @@ function buildQueryString(params) {
|
|
|
93
93
|
if (params.limit != null) parts.push(`limit=${params.limit}`);
|
|
94
94
|
if (params.offset != null) parts.push(`offset=${params.offset}`);
|
|
95
95
|
if (params.page != null) parts.push(`page=${params.page}`);
|
|
96
|
+
if (params.after) parts.push(`after=${encodeURIComponent(params.after)}`);
|
|
96
97
|
if (params.orderBy) {
|
|
97
98
|
const wire = serializeOrderBy(params.orderBy);
|
|
98
99
|
if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
|
|
@@ -108,7 +109,10 @@ function buildQueryString(params) {
|
|
|
108
109
|
if (vs.distance) parts.push(`vector_distance=${encodeURIComponent(vs.distance)}`);
|
|
109
110
|
if (vs.threshold !== void 0) parts.push(`vector_threshold=${encodeURIComponent(String(vs.threshold))}`);
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
const include = serializeInclude(params.include);
|
|
113
|
+
if (include) parts.push(`include=${encodeURIComponent(include)}`);
|
|
114
|
+
if (params.fields && params.fields.length > 0) parts.push(`fields=${encodeURIComponent(params.fields.join(","))}`);
|
|
115
|
+
if (params.distinct) parts.push("distinct=true");
|
|
112
116
|
if (params.logical) {
|
|
113
117
|
const root = params.logical;
|
|
114
118
|
const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
@@ -123,6 +127,39 @@ function buildQueryString(params) {
|
|
|
123
127
|
return parts.length > 0 ? "?" + parts.join("&") : "";
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
130
|
+
* The query string for `GET /<collection>/aggregate`.
|
|
131
|
+
*
|
|
132
|
+
* `?select=sum(total),count()` is SQL's spelling, because whoever writes an
|
|
133
|
+
* aggregate is thinking in SQL and any other spelling has to be learned first —
|
|
134
|
+
* and because it is what the route already parses. The filters are serialised
|
|
135
|
+
* by exactly the same code a `find()` uses, so "revenue by status, this month"
|
|
136
|
+
* narrows the same rows whichever call is asking.
|
|
137
|
+
*
|
|
138
|
+
* `orderBy`, `include` and the page are deliberately not here: an aggregate has
|
|
139
|
+
* no rows to sort, no relations to load and no page to continue. `limit` is,
|
|
140
|
+
* and bounds the number of *groups*.
|
|
141
|
+
*/
|
|
142
|
+
function buildAggregateQueryString(params) {
|
|
143
|
+
const parts = [];
|
|
144
|
+
const select = params.select.map((entry) => `${entry.fn}(${entry.field ?? ""})`).join(",");
|
|
145
|
+
parts.push(`select=${encodeURIComponent(select)}`);
|
|
146
|
+
if (params.groupBy && params.groupBy.length > 0) parts.push(`groupBy=${encodeURIComponent(params.groupBy.join(","))}`);
|
|
147
|
+
if (params.limit != null) parts.push(`limit=${params.limit}`);
|
|
148
|
+
if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
|
|
149
|
+
if (params.logical) {
|
|
150
|
+
const root = params.logical;
|
|
151
|
+
const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
152
|
+
parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
|
|
153
|
+
}
|
|
154
|
+
if (params.where) {
|
|
155
|
+
assertNoUndefinedFilterValues(params.where);
|
|
156
|
+
const serialized = serializeFilter(params.where);
|
|
157
|
+
for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
|
|
158
|
+
else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
|
|
159
|
+
}
|
|
160
|
+
return "?" + parts.join("&");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
126
163
|
* The base every request and every caller-built URL resolves against.
|
|
127
164
|
*
|
|
128
165
|
* `baseUrl` is optional because the common production shape is a Rebase
|
|
@@ -284,7 +321,7 @@ function createTransport(config, environment) {
|
|
|
284
321
|
})()
|
|
285
322
|
});
|
|
286
323
|
}
|
|
287
|
-
async function request(path, init) {
|
|
324
|
+
async function request(path, init, meta) {
|
|
288
325
|
const url = resolveBaseUrl(config.baseUrl) + apiPath + path;
|
|
289
326
|
let activeToken = token;
|
|
290
327
|
if (tokenGetter) try {
|
|
@@ -298,6 +335,10 @@ function createTransport(config, environment) {
|
|
|
298
335
|
...init,
|
|
299
336
|
headers
|
|
300
337
|
});
|
|
338
|
+
if (meta) {
|
|
339
|
+
meta.status = res.status;
|
|
340
|
+
meta.etag = res.headers?.get?.("ETag") ?? void 0;
|
|
341
|
+
}
|
|
301
342
|
if (res.status === 204) return void 0;
|
|
302
343
|
const text = await res.text().catch(() => "");
|
|
303
344
|
let body = {};
|
|
@@ -334,6 +375,10 @@ function createTransport(config, environment) {
|
|
|
334
375
|
...init,
|
|
335
376
|
headers: retryHeaders
|
|
336
377
|
});
|
|
378
|
+
if (meta) {
|
|
379
|
+
meta.status = retryRes.status;
|
|
380
|
+
meta.etag = retryRes.headers?.get?.("ETag") ?? void 0;
|
|
381
|
+
}
|
|
337
382
|
if (retryRes.status === 204) return void 0;
|
|
338
383
|
const retryText = await retryRes.text().catch(() => "");
|
|
339
384
|
let retryBody = {};
|
|
@@ -1543,9 +1588,14 @@ var SDKQueryBuilder = class {
|
|
|
1543
1588
|
* `.orderBy("roles").orderBy("created_at", "desc")` sorts by role and
|
|
1544
1589
|
* shows the newest first within each one.
|
|
1545
1590
|
*/
|
|
1546
|
-
orderBy(column, direction = "asc") {
|
|
1591
|
+
orderBy(column, direction = "asc", nulls) {
|
|
1547
1592
|
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
1548
|
-
|
|
1593
|
+
const key = sortKeyToString(column);
|
|
1594
|
+
this.params.orderBy = [...existing, nulls ? [
|
|
1595
|
+
key,
|
|
1596
|
+
direction,
|
|
1597
|
+
nulls
|
|
1598
|
+
] : [key, direction]];
|
|
1549
1599
|
return this;
|
|
1550
1600
|
}
|
|
1551
1601
|
/**
|
|
@@ -1616,15 +1666,74 @@ var SDKQueryBuilder = class {
|
|
|
1616
1666
|
return this;
|
|
1617
1667
|
}
|
|
1618
1668
|
/**
|
|
1619
|
-
*
|
|
1620
|
-
* Relations will be populated with full data instead of just IDs.
|
|
1669
|
+
* Load related rows into the response, in place of their foreign keys.
|
|
1621
1670
|
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1671
|
+
* Three spellings, all the same request:
|
|
1672
|
+
*
|
|
1673
|
+
* ```ts
|
|
1674
|
+
* client.data.posts.include("tags", "author") // names
|
|
1675
|
+
* client.data.posts.include("comments.author") // a dotted path
|
|
1676
|
+
* client.data.posts.include({ // parametrised
|
|
1677
|
+
* comments: {
|
|
1678
|
+
* limit: 5,
|
|
1679
|
+
* where: { published: ["==", true] },
|
|
1680
|
+
* orderBy: ["created_at", "desc"],
|
|
1681
|
+
* include: { author: true }
|
|
1682
|
+
* }
|
|
1683
|
+
* })
|
|
1684
|
+
* ```
|
|
1685
|
+
*
|
|
1686
|
+
* Up to three hops deep, and `"*"` still loads every relation one hop
|
|
1687
|
+
* deep. A name that is not a relation of the collection is a 400
|
|
1688
|
+
* `UNKNOWN_RELATION` — it used to be ignored, which answers 200 with the
|
|
1689
|
+
* field missing and reads exactly like a row that has no related row.
|
|
1690
|
+
*
|
|
1691
|
+
* Repeated calls **merge**: `.include("author").include("tags")` asks for
|
|
1692
|
+
* both. Assigning here — which is what it used to do — meant the second
|
|
1693
|
+
* call silently discarded the first.
|
|
1625
1694
|
*/
|
|
1626
1695
|
include(...relations) {
|
|
1627
|
-
this.params.include = relations;
|
|
1696
|
+
this.params.include = mergeIncludeSpecs(this.params.include, relations);
|
|
1697
|
+
return this;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Return only these columns.
|
|
1701
|
+
*
|
|
1702
|
+
* A projection at the database, not a trim of the response: a query that
|
|
1703
|
+
* needs two fields of a wide row reads two columns. The primary key always
|
|
1704
|
+
* comes back regardless — a row that cannot be addressed cannot be
|
|
1705
|
+
* updated, deleted or paged past — and `excludeFromApi` columns stay
|
|
1706
|
+
* hidden whether or not they are named.
|
|
1707
|
+
*/
|
|
1708
|
+
fields(...columns) {
|
|
1709
|
+
this.params.fields = [...this.params.fields ?? [], ...columns];
|
|
1710
|
+
return this;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Collapse rows that are identical over the columns being returned.
|
|
1714
|
+
*
|
|
1715
|
+
* Only meaningful alongside {@link fields}: the primary key is always in
|
|
1716
|
+
* the projection, so without narrowing it every row is already distinct.
|
|
1717
|
+
*
|
|
1718
|
+
* ```ts
|
|
1719
|
+
* client.data.posts.fields("status").distinct().find() // the statuses in use
|
|
1720
|
+
* ```
|
|
1721
|
+
*/
|
|
1722
|
+
distinct(enabled = true) {
|
|
1723
|
+
this.params.distinct = enabled;
|
|
1724
|
+
return this;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Continue after a previous page's `meta.nextCursor` — keyset paging.
|
|
1728
|
+
*
|
|
1729
|
+
* Unlike `offset`, a row inserted or deleted before the cursor cannot
|
|
1730
|
+
* shift the window, so a walk neither repeats nor skips rows. Keep
|
|
1731
|
+
* `orderBy` identical across pages: a cursor only continues the listing it
|
|
1732
|
+
* came from, and one used against a different sort is refused rather than
|
|
1733
|
+
* seeked in an order nobody asked for.
|
|
1734
|
+
*/
|
|
1735
|
+
after(cursor) {
|
|
1736
|
+
this.params.after = cursor;
|
|
1628
1737
|
return this;
|
|
1629
1738
|
}
|
|
1630
1739
|
/**
|
|
@@ -1634,6 +1743,29 @@ var SDKQueryBuilder = class {
|
|
|
1634
1743
|
return this.collection.find(this.params);
|
|
1635
1744
|
}
|
|
1636
1745
|
/**
|
|
1746
|
+
* Aggregate the rows this query matches instead of returning them.
|
|
1747
|
+
*
|
|
1748
|
+
* The builder's `where`/`logical`/`search` narrow which rows are
|
|
1749
|
+
* aggregated; its `orderBy`, `include` and window do not apply — an
|
|
1750
|
+
* aggregate has no rows to sort, no relations to load and no page to
|
|
1751
|
+
* continue.
|
|
1752
|
+
*
|
|
1753
|
+
* ```ts
|
|
1754
|
+
* await client.data.orders
|
|
1755
|
+
* .where("created_at", ">=", startOfMonth)
|
|
1756
|
+
* .aggregate({ select: [{ fn: "sum", field: "total" }], groupBy: ["status"] });
|
|
1757
|
+
* // [{ status: "paid", sum_total: 41822.5 }, …]
|
|
1758
|
+
* ```
|
|
1759
|
+
*/
|
|
1760
|
+
async aggregate(params) {
|
|
1761
|
+
return this.collection.aggregate({
|
|
1762
|
+
...params,
|
|
1763
|
+
where: this.params.where,
|
|
1764
|
+
logical: this.params.logical,
|
|
1765
|
+
searchString: this.params.searchString
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1637
1769
|
* Page through everything this query matches, one row at a time.
|
|
1638
1770
|
*
|
|
1639
1771
|
* `.limit()` on the builder becomes the page size, so the ceiling on a
|
|
@@ -1709,6 +1841,67 @@ function inflightCountsFor(transport) {
|
|
|
1709
1841
|
}
|
|
1710
1842
|
return map;
|
|
1711
1843
|
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Where a fetched row's version is kept.
|
|
1846
|
+
*
|
|
1847
|
+
* Non-enumerable, so it does not appear in `Object.keys`, in `JSON.stringify`,
|
|
1848
|
+
* in a spread into an update body, or in the generated `Row` type — a row's
|
|
1849
|
+
* version is a fact about the *response*, not a column, and putting it in the
|
|
1850
|
+
* row proper would mean the server having to strip it back off every write.
|
|
1851
|
+
*
|
|
1852
|
+
* A string key rather than a symbol so it survives the structured clone an
|
|
1853
|
+
* offline store or a worker boundary does… which it does not, and that is the
|
|
1854
|
+
* honest position: an etag is per-read, and a row that has been through a cache
|
|
1855
|
+
* has not been read from the server. `etagOf` answers `undefined` there, and a
|
|
1856
|
+
* conditional write without a tag is an unconditional one.
|
|
1857
|
+
*/
|
|
1858
|
+
var ETAG_KEY = "__etag";
|
|
1859
|
+
/**
|
|
1860
|
+
* The version of a row, when it came straight from a read that reported one.
|
|
1861
|
+
*
|
|
1862
|
+
* ```ts
|
|
1863
|
+
* const post = await client.data.posts.get(1);
|
|
1864
|
+
* await client.data.posts.update(1, { title: "New" }, { ifMatch: etagOf(post) });
|
|
1865
|
+
* ```
|
|
1866
|
+
*
|
|
1867
|
+
* `undefined` for a row from `find()` (a list response carries one ETag at
|
|
1868
|
+
* most, and it would not be this row's), from the offline cache, or from a
|
|
1869
|
+
* server that does not send them. Passing `undefined` as `ifMatch` sends no
|
|
1870
|
+
* precondition, so the call above degrades to an ordinary update rather than
|
|
1871
|
+
* failing — which is the right default, and the reason to read this at the call
|
|
1872
|
+
* site where the choice is visible.
|
|
1873
|
+
*/
|
|
1874
|
+
function etagOf(row) {
|
|
1875
|
+
if (!row || typeof row !== "object") return void 0;
|
|
1876
|
+
const value = row[ETAG_KEY];
|
|
1877
|
+
return typeof value === "string" ? value : void 0;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* The headers a write's options ask for.
|
|
1881
|
+
*
|
|
1882
|
+
* One function rather than a spread at each call site: the three headers are
|
|
1883
|
+
* easy to add to one method and forget on the next, which is exactly how single
|
|
1884
|
+
* `PATCH` and `DELETE` came to send an `Idempotency-Key` the server never read
|
|
1885
|
+
* and `delete()` came to have no options at all.
|
|
1886
|
+
*/
|
|
1887
|
+
function writeHeaders(options) {
|
|
1888
|
+
const headers = {};
|
|
1889
|
+
if (options?.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
1890
|
+
if (options?.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
1891
|
+
if (options?.returning === false) headers["Prefer"] = "return=minimal";
|
|
1892
|
+
return Object.keys(headers).length > 0 ? { headers } : void 0;
|
|
1893
|
+
}
|
|
1894
|
+
/** Attach a version to a row without making it part of the row. */
|
|
1895
|
+
function withETag(row, etag) {
|
|
1896
|
+
if (!etag || !row || typeof row !== "object") return row;
|
|
1897
|
+
Object.defineProperty(row, ETAG_KEY, {
|
|
1898
|
+
value: etag,
|
|
1899
|
+
enumerable: false,
|
|
1900
|
+
configurable: true,
|
|
1901
|
+
writable: true
|
|
1902
|
+
});
|
|
1903
|
+
return row;
|
|
1904
|
+
}
|
|
1712
1905
|
function createCollectionClient(transport, slug, ws) {
|
|
1713
1906
|
const basePath = `/data/${slug}`;
|
|
1714
1907
|
const client = {
|
|
@@ -1728,9 +1921,10 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1728
1921
|
},
|
|
1729
1922
|
async findById(id) {
|
|
1730
1923
|
try {
|
|
1731
|
-
const
|
|
1924
|
+
const meta = {};
|
|
1925
|
+
const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" }, meta);
|
|
1732
1926
|
if (!raw) return void 0;
|
|
1733
|
-
return raw;
|
|
1927
|
+
return withETag(raw, meta.etag);
|
|
1734
1928
|
} catch (err) {
|
|
1735
1929
|
if (err instanceof RebaseApiError && err.status === 404) return;
|
|
1736
1930
|
throw err;
|
|
@@ -1750,20 +1944,38 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1750
1944
|
return await transport.request(basePath, {
|
|
1751
1945
|
method: "POST",
|
|
1752
1946
|
body: JSON.stringify(body),
|
|
1753
|
-
...options
|
|
1947
|
+
...writeHeaders(options)
|
|
1948
|
+
});
|
|
1949
|
+
},
|
|
1950
|
+
/**
|
|
1951
|
+
* Insert, or replace the row already occupying the conflict target.
|
|
1952
|
+
*
|
|
1953
|
+
* One statement server-side (`INSERT ... ON CONFLICT DO UPDATE`), so
|
|
1954
|
+
* unlike a `findById` followed by `create`-or-`update` it cannot lose
|
|
1955
|
+
* the race between the two.
|
|
1956
|
+
*/
|
|
1957
|
+
async upsert(data, options) {
|
|
1958
|
+
const query = options?.onConflict?.length ? `?on_conflict=${encodeURIComponent(options.onConflict.join(","))}` : "";
|
|
1959
|
+
return await transport.request(`${basePath}${query}`, {
|
|
1960
|
+
method: "POST",
|
|
1961
|
+
body: JSON.stringify(data),
|
|
1962
|
+
...writeHeaders(options)
|
|
1754
1963
|
});
|
|
1755
1964
|
},
|
|
1756
1965
|
async createMany(data, options) {
|
|
1757
1966
|
if (!Array.isArray(data)) throw new TypeError("createMany expects an array of records.");
|
|
1758
1967
|
if (data.length === 0) return [];
|
|
1759
|
-
|
|
1968
|
+
const raw = await transport.request(`${basePath}/bulk`, {
|
|
1760
1969
|
method: "POST",
|
|
1761
1970
|
body: JSON.stringify({
|
|
1762
1971
|
rows: data,
|
|
1763
|
-
...options?.upsert ? { upsert: true } : {}
|
|
1972
|
+
...options?.upsert ? { upsert: true } : {},
|
|
1973
|
+
...options?.onConflict?.length ? { onConflict: options.onConflict } : {}
|
|
1764
1974
|
}),
|
|
1765
|
-
...options
|
|
1766
|
-
})
|
|
1975
|
+
...writeHeaders(options)
|
|
1976
|
+
});
|
|
1977
|
+
if (options?.returning === false) return [];
|
|
1978
|
+
return raw.data || [];
|
|
1767
1979
|
},
|
|
1768
1980
|
/**
|
|
1769
1981
|
* `PATCH`, the verb for a merge — `update(id, data: Partial<M>)` sends
|
|
@@ -1778,20 +1990,25 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1778
1990
|
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
1779
1991
|
method: "PATCH",
|
|
1780
1992
|
body: JSON.stringify(data),
|
|
1781
|
-
...options
|
|
1993
|
+
...writeHeaders(options)
|
|
1782
1994
|
});
|
|
1783
1995
|
},
|
|
1784
1996
|
async updateMany(updates, options) {
|
|
1785
1997
|
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
1786
1998
|
if (updates.length === 0) return [];
|
|
1787
|
-
|
|
1999
|
+
const raw = await transport.request(`${basePath}/bulk`, {
|
|
1788
2000
|
method: "PATCH",
|
|
1789
2001
|
body: JSON.stringify({ updates }),
|
|
1790
|
-
...options
|
|
1791
|
-
})
|
|
2002
|
+
...writeHeaders(options)
|
|
2003
|
+
});
|
|
2004
|
+
if (options?.returning === false) return [];
|
|
2005
|
+
return raw.data || [];
|
|
1792
2006
|
},
|
|
1793
|
-
async delete(id) {
|
|
1794
|
-
await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
2007
|
+
async delete(id, options) {
|
|
2008
|
+
await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
2009
|
+
method: "DELETE",
|
|
2010
|
+
...writeHeaders(options)
|
|
2011
|
+
});
|
|
1795
2012
|
},
|
|
1796
2013
|
/**
|
|
1797
2014
|
* `POST .../bulk/delete`, not `DELETE .../bulk`.
|
|
@@ -1810,7 +2027,7 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1810
2027
|
await transport.request(`${basePath}/bulk/delete`, {
|
|
1811
2028
|
method: "POST",
|
|
1812
2029
|
body: JSON.stringify({ ids }),
|
|
1813
|
-
...options
|
|
2030
|
+
...writeHeaders(options)
|
|
1814
2031
|
});
|
|
1815
2032
|
},
|
|
1816
2033
|
async count(params) {
|
|
@@ -1889,8 +2106,8 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1889
2106
|
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
1890
2107
|
return builder.where(columnOrCondition, operator, value);
|
|
1891
2108
|
},
|
|
1892
|
-
orderBy(column, direction) {
|
|
1893
|
-
return new SDKQueryBuilder(client).orderBy(column, direction);
|
|
2109
|
+
orderBy(column, direction, nulls) {
|
|
2110
|
+
return new SDKQueryBuilder(client).orderBy(column, direction, nulls);
|
|
1894
2111
|
},
|
|
1895
2112
|
limit(count) {
|
|
1896
2113
|
return new SDKQueryBuilder(client).limit(count);
|
|
@@ -1907,6 +2124,28 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1907
2124
|
include(...relations) {
|
|
1908
2125
|
return new SDKQueryBuilder(client).include(...relations);
|
|
1909
2126
|
},
|
|
2127
|
+
fields(...columns) {
|
|
2128
|
+
return new SDKQueryBuilder(client).fields(...columns);
|
|
2129
|
+
},
|
|
2130
|
+
distinct(enabled) {
|
|
2131
|
+
return new SDKQueryBuilder(client).distinct(enabled);
|
|
2132
|
+
},
|
|
2133
|
+
after(cursor) {
|
|
2134
|
+
return new SDKQueryBuilder(client).after(cursor);
|
|
2135
|
+
},
|
|
2136
|
+
/**
|
|
2137
|
+
* `count`/`sum`/`avg`/`min`/`max` over the matching rows, optionally
|
|
2138
|
+
* grouped — `GET /<collection>/aggregate`.
|
|
2139
|
+
*
|
|
2140
|
+
* The REST route has served this since aggregates landed and the SDK
|
|
2141
|
+
* had no method for it, so the only way to reach it from a typed client
|
|
2142
|
+
* was to hand-build the URL. `findAll()` and a reduce is the thing this
|
|
2143
|
+
* exists to replace: wrong under a `limit`, unaffordable without one.
|
|
2144
|
+
*/
|
|
2145
|
+
async aggregate(params) {
|
|
2146
|
+
const qs = buildAggregateQueryString(params);
|
|
2147
|
+
return (await transport.request(`${basePath}/aggregate${qs}`, { method: "GET" })).data || [];
|
|
2148
|
+
},
|
|
1910
2149
|
listen: unsupportedMethod(NO_SOCKET),
|
|
1911
2150
|
listenById: unsupportedMethod(NO_SOCKET)
|
|
1912
2151
|
};
|
|
@@ -1922,36 +2161,46 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1922
2161
|
logical: params?.logical,
|
|
1923
2162
|
limit: params?.limit,
|
|
1924
2163
|
offset: window.driverOffset,
|
|
2164
|
+
page: params?.page,
|
|
2165
|
+
include: params?.include,
|
|
2166
|
+
fields: params?.fields,
|
|
2167
|
+
distinct: params?.distinct,
|
|
1925
2168
|
orderBy: normalizeOrderBy(params?.orderBy),
|
|
1926
2169
|
searchString: params?.searchString,
|
|
1927
2170
|
searchExplain: params?.searchExplain,
|
|
1928
2171
|
vectorSearch: params?.vectorSearch
|
|
1929
|
-
}, (incomingRows) => {
|
|
2172
|
+
}, (incomingRows, frameMeta) => {
|
|
1930
2173
|
const currentUpdateId = ++lastUpdateId;
|
|
1931
2174
|
const requestedLimit = window.limit;
|
|
1932
2175
|
const offset = window.offset;
|
|
1933
2176
|
const rows = incomingRows;
|
|
1934
|
-
const emit = (total, hasMore) => {
|
|
2177
|
+
const emit = (total, hasMore, nextCursor) => {
|
|
1935
2178
|
if (!active || currentUpdateId !== lastUpdateId) return;
|
|
1936
2179
|
onUpdate({
|
|
1937
2180
|
data: rows,
|
|
1938
2181
|
meta: {
|
|
1939
2182
|
total,
|
|
1940
|
-
limit: requestedLimit,
|
|
1941
|
-
offset,
|
|
1942
|
-
hasMore
|
|
2183
|
+
limit: frameMeta?.limit ?? requestedLimit,
|
|
2184
|
+
offset: frameMeta?.offset ?? offset,
|
|
2185
|
+
hasMore,
|
|
2186
|
+
...nextCursor && { nextCursor }
|
|
1943
2187
|
}
|
|
1944
2188
|
});
|
|
1945
2189
|
};
|
|
2190
|
+
if (frameMeta && frameMeta.total !== void 0) {
|
|
2191
|
+
lastKnownTotal = frameMeta.total;
|
|
2192
|
+
emit(frameMeta.total, frameMeta.hasMore, frameMeta.nextCursor);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
if (lastKnownTotal !== void 0) {
|
|
2196
|
+
emit(lastKnownTotal, offset + rows.length < lastKnownTotal, frameMeta?.nextCursor);
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
1946
2199
|
client.count(params).then((total) => {
|
|
1947
2200
|
lastKnownTotal = total;
|
|
1948
|
-
emit(total, offset + rows.length < total);
|
|
2201
|
+
emit(total, offset + rows.length < total, frameMeta?.nextCursor);
|
|
1949
2202
|
}).catch(() => {
|
|
1950
|
-
|
|
1951
|
-
emit(lastKnownTotal, offset + rows.length < lastKnownTotal);
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
emit(offset + rows.length, rows.length >= requestedLimit);
|
|
2203
|
+
emit(offset + rows.length, rows.length >= requestedLimit, frameMeta?.nextCursor);
|
|
1955
2204
|
});
|
|
1956
2205
|
}, onError);
|
|
1957
2206
|
return () => {
|
|
@@ -2044,9 +2293,13 @@ function createStorage(transport, storageId) {
|
|
|
2044
2293
|
if (!storageId) return path;
|
|
2045
2294
|
return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
|
|
2046
2295
|
};
|
|
2047
|
-
async function putObject({ file, key, metadata, bucket, public: isPublic }) {
|
|
2296
|
+
async function putObject({ file, key, metadata, bucket, public: isPublic, context }) {
|
|
2048
2297
|
const formData = new FormData();
|
|
2049
2298
|
formData.append("file", file);
|
|
2299
|
+
if (context?.collection && context?.property) {
|
|
2300
|
+
formData.append("collection", context.collection);
|
|
2301
|
+
formData.append("property", context.property);
|
|
2302
|
+
}
|
|
2050
2303
|
let effectiveKey = key;
|
|
2051
2304
|
if (isPublic && effectiveKey && !isPublicStoragePath(effectiveKey)) effectiveKey = `${PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
|
|
2052
2305
|
if (effectiveKey) formData.append("key", effectiveKey);
|
|
@@ -2628,6 +2881,8 @@ var RebaseWebSocketClient = class {
|
|
|
2628
2881
|
const incomingRows = message.rows || [];
|
|
2629
2882
|
const updatePks = message.pks;
|
|
2630
2883
|
if (updatePks) collectionSub.pks = updatePks;
|
|
2884
|
+
const updateMeta = message.meta;
|
|
2885
|
+
if (updateMeta) collectionSub.latestMeta = updateMeta;
|
|
2631
2886
|
const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
|
|
2632
2887
|
collectionSub.latestData = rows;
|
|
2633
2888
|
collectionSub.lastUpdated = Date.now();
|
|
@@ -2637,7 +2892,7 @@ var RebaseWebSocketClient = class {
|
|
|
2637
2892
|
collectionSub.subscribeInFlight = false;
|
|
2638
2893
|
collectionSub.callbacks.forEach((callback) => {
|
|
2639
2894
|
try {
|
|
2640
|
-
callback.onUpdate(rows);
|
|
2895
|
+
callback.onUpdate(rows, collectionSub.latestMeta);
|
|
2641
2896
|
} catch (error) {
|
|
2642
2897
|
console.error("Error in collection subscription callback:", error);
|
|
2643
2898
|
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -3043,7 +3298,7 @@ var RebaseWebSocketClient = class {
|
|
|
3043
3298
|
onError
|
|
3044
3299
|
});
|
|
3045
3300
|
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
|
|
3046
|
-
onUpdate(existingSubscription.latestData);
|
|
3301
|
+
onUpdate(existingSubscription.latestData, existingSubscription.latestMeta);
|
|
3047
3302
|
} catch (error) {
|
|
3048
3303
|
console.error("Error in collection subscription callback:", error);
|
|
3049
3304
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -4565,9 +4820,22 @@ function logicalOrders(condition, depth = 0) {
|
|
|
4565
4820
|
* separate answer, because a sort changes which rows come first and not which
|
|
4566
4821
|
* rows match. See {@link isLocallySortable}.
|
|
4567
4822
|
*/
|
|
4823
|
+
/**
|
|
4824
|
+
* Whether an `include` asks for anything, in any of its spellings.
|
|
4825
|
+
*
|
|
4826
|
+
* `include` is a list of names, a list of dotted paths, or a tree — and only
|
|
4827
|
+
* the first two have a `length`. A `.length > 0` test read the tree form as
|
|
4828
|
+
* "no relations", which is the answer that makes the local cache claim it can
|
|
4829
|
+
* evaluate a relational query exactly.
|
|
4830
|
+
*/
|
|
4831
|
+
function hasAnyInclude(include) {
|
|
4832
|
+
if (!include) return false;
|
|
4833
|
+
if (Array.isArray(include)) return include.length > 0;
|
|
4834
|
+
return Object.keys(include).length > 0;
|
|
4835
|
+
}
|
|
4568
4836
|
function isExactlyEvaluable(params) {
|
|
4569
4837
|
if (!params) return true;
|
|
4570
|
-
if (params.include
|
|
4838
|
+
if (hasAnyInclude(params.include)) return false;
|
|
4571
4839
|
if (params.searchString) return false;
|
|
4572
4840
|
if (params.vectorSearch) return false;
|
|
4573
4841
|
if (whereOrders(params.where)) return false;
|
|
@@ -4980,6 +5248,47 @@ var OfflineManager = class {
|
|
|
4980
5248
|
this.notifyCollection(slug);
|
|
4981
5249
|
return rows;
|
|
4982
5250
|
},
|
|
5251
|
+
/**
|
|
5252
|
+
* Upsert, with the one case that cannot be resolved without the
|
|
5253
|
+
* server refused rather than guessed.
|
|
5254
|
+
*
|
|
5255
|
+
* On the primary key an upsert is create-or-replace, which the
|
|
5256
|
+
* local store can do and the queue already carries (`createMany`
|
|
5257
|
+
* with `upsert`). On a natural key it is not: deciding whether a
|
|
5258
|
+
* row with that email exists means an index this store does not
|
|
5259
|
+
* keep, and inventing an id would insert a duplicate the moment the
|
|
5260
|
+
* queue drains — the exact outcome upsert exists to prevent.
|
|
5261
|
+
*/
|
|
5262
|
+
upsert: async (data, options) => {
|
|
5263
|
+
await this.ensureCollection(slug);
|
|
5264
|
+
if (this.connectivity.shouldAttempt()) try {
|
|
5265
|
+
const row = await inner.upsert(data, options);
|
|
5266
|
+
this.connectivity.markSuccess();
|
|
5267
|
+
await this.ingest(slug, [row]);
|
|
5268
|
+
this.notifyCollection(slug);
|
|
5269
|
+
this.scheduleRefresh(slug);
|
|
5270
|
+
return row;
|
|
5271
|
+
} catch (error) {
|
|
5272
|
+
if (!isNetworkError(error)) throw error;
|
|
5273
|
+
this.connectivity.markFailure();
|
|
5274
|
+
}
|
|
5275
|
+
const rowId = data.id;
|
|
5276
|
+
if (options?.onConflict?.length || rowId === void 0) throw new RebaseClientError(`Cannot upsert into "${slug}" while offline: the row it would replace can only be found by the server. Upserting on the primary key, with the key in the row, is queued; upserting on a natural key is not.`, { code: "OFFLINE_UPSERT_UNSUPPORTED" });
|
|
5277
|
+
const row = {
|
|
5278
|
+
...data,
|
|
5279
|
+
id: rowId
|
|
5280
|
+
};
|
|
5281
|
+
await this.enqueue({
|
|
5282
|
+
collection: slug,
|
|
5283
|
+
type: "createMany",
|
|
5284
|
+
data: [row],
|
|
5285
|
+
upsert: true,
|
|
5286
|
+
rollback: { rows: { [String(rowId)]: this.rawLocalRow(slug, rowId) ?? null } }
|
|
5287
|
+
});
|
|
5288
|
+
this.setLocalRow(slug, rowId, row);
|
|
5289
|
+
this.notifyCollection(slug);
|
|
5290
|
+
return row;
|
|
5291
|
+
},
|
|
4983
5292
|
updateMany: async (updates, options) => {
|
|
4984
5293
|
await this.ensureCollection(slug);
|
|
4985
5294
|
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
@@ -5046,10 +5355,10 @@ var OfflineManager = class {
|
|
|
5046
5355
|
for (const id of ids) this.removeLocalRow(slug, id, false);
|
|
5047
5356
|
this.notifyCollection(slug);
|
|
5048
5357
|
},
|
|
5049
|
-
update: async (id, data) => {
|
|
5358
|
+
update: async (id, data, options) => {
|
|
5050
5359
|
await this.ensureCollection(slug);
|
|
5051
5360
|
if (this.connectivity.shouldAttempt() && !this.hasPending(slug, id)) try {
|
|
5052
|
-
const row = await inner.update(id, data);
|
|
5361
|
+
const row = await inner.update(id, data, options);
|
|
5053
5362
|
this.connectivity.markSuccess();
|
|
5054
5363
|
await this.ingest(slug, [row]);
|
|
5055
5364
|
this.notifyCollection(slug);
|
|
@@ -5058,6 +5367,7 @@ var OfflineManager = class {
|
|
|
5058
5367
|
if (!isNetworkError(error)) throw error;
|
|
5059
5368
|
this.connectivity.markFailure();
|
|
5060
5369
|
}
|
|
5370
|
+
if (hasFieldOperation(data)) throw new RebaseClientError(`Cannot apply a field operation to "${slug}" while offline: it is evaluated against the stored value, which this device does not have a current copy of. Send the value itself, or retry when the connection is back.`, { code: "OFFLINE_FIELD_OP_UNSUPPORTED" });
|
|
5061
5371
|
const base = this.rawLocalRow(slug, id);
|
|
5062
5372
|
await this.enqueue({
|
|
5063
5373
|
collection: slug,
|
|
@@ -5075,10 +5385,10 @@ var OfflineManager = class {
|
|
|
5075
5385
|
this.notifyCollection(slug);
|
|
5076
5386
|
return optimistic;
|
|
5077
5387
|
},
|
|
5078
|
-
delete: async (id) => {
|
|
5388
|
+
delete: async (id, options) => {
|
|
5079
5389
|
await this.ensureCollection(slug);
|
|
5080
5390
|
if (this.connectivity.shouldAttempt() && !this.hasPending(slug, id)) try {
|
|
5081
|
-
await inner.delete(id);
|
|
5391
|
+
await inner.delete(id, options);
|
|
5082
5392
|
this.connectivity.markSuccess();
|
|
5083
5393
|
this.removeLocalRow(slug, id, true);
|
|
5084
5394
|
this.notifyCollection(slug);
|
|
@@ -5121,12 +5431,16 @@ var OfflineManager = class {
|
|
|
5121
5431
|
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
5122
5432
|
return builder.where(columnOrCondition, operator, value);
|
|
5123
5433
|
},
|
|
5124
|
-
orderBy: (column, direction) => new SDKQueryBuilder(wrapped).orderBy(column, direction),
|
|
5434
|
+
orderBy: (column, direction, nulls) => new SDKQueryBuilder(wrapped).orderBy(column, direction, nulls),
|
|
5125
5435
|
limit: (count) => new SDKQueryBuilder(wrapped).limit(count),
|
|
5126
5436
|
offset: (count) => new SDKQueryBuilder(wrapped).offset(count),
|
|
5127
5437
|
search: (searchString, options) => new SDKQueryBuilder(wrapped).search(searchString, options),
|
|
5128
5438
|
vectorSearch: (property, vector, options) => new SDKQueryBuilder(wrapped).vectorSearch(property, vector, options),
|
|
5129
5439
|
include: (...relations) => new SDKQueryBuilder(wrapped).include(...relations),
|
|
5440
|
+
fields: (...columns) => new SDKQueryBuilder(wrapped).fields(...columns),
|
|
5441
|
+
distinct: (enabled) => new SDKQueryBuilder(wrapped).distinct(enabled),
|
|
5442
|
+
after: (cursor) => new SDKQueryBuilder(wrapped).after(cursor),
|
|
5443
|
+
aggregate: inner.aggregate,
|
|
5130
5444
|
listen: inner.listen,
|
|
5131
5445
|
listenById: inner.listenById
|
|
5132
5446
|
};
|
|
@@ -6274,11 +6588,26 @@ channel: (name, options) => {
|
|
|
6274
6588
|
body: payload ? JSON.stringify(payload) : void 0
|
|
6275
6589
|
});
|
|
6276
6590
|
},
|
|
6591
|
+
batch: async (operations, options) => {
|
|
6592
|
+
if (!Array.isArray(operations)) throw new TypeError("batch expects an array of operations.");
|
|
6593
|
+
if (operations.length === 0) return {
|
|
6594
|
+
data: [],
|
|
6595
|
+
meta: { operations: 0 }
|
|
6596
|
+
};
|
|
6597
|
+
const headers = {};
|
|
6598
|
+
if (options?.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
6599
|
+
if (options?.returning === false) headers.Prefer = "return=minimal";
|
|
6600
|
+
return transport.request("/data/_batch", {
|
|
6601
|
+
method: "POST",
|
|
6602
|
+
body: JSON.stringify({ operations }),
|
|
6603
|
+
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
6604
|
+
});
|
|
6605
|
+
},
|
|
6277
6606
|
data: dataProxy,
|
|
6278
6607
|
...offlineManager ? { offline: offlineManager.api } : {}
|
|
6279
6608
|
};
|
|
6280
6609
|
}
|
|
6281
6610
|
//#endregion
|
|
6282
|
-
export { MemoryOfflineStore, QueryBuilder, RebaseApiError, RebaseClientError, RebasePaginationError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, isOfflineError, or };
|
|
6611
|
+
export { MemoryOfflineStore, QueryBuilder, RebaseApiError, RebaseClientError, RebasePaginationError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, etagOf, isOfflineError, not, or };
|
|
6283
6612
|
|
|
6284
6613
|
//# sourceMappingURL=index.es.js.map
|