@rebasepro/common 0.23.0 → 0.24.1-canary.g56af4a4
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/collections/CollectionRegistry.d.ts +72 -3
- package/dist/collections/CollectionRegistry.es.js +404 -0
- package/dist/collections/CollectionRegistry.es.js.map +1 -0
- package/dist/collections/auth-secrets.es.js +47 -0
- package/dist/collections/auth-secrets.es.js.map +1 -0
- package/dist/collections/default-collections.es.js +120 -0
- package/dist/collections/default-collections.es.js.map +1 -0
- package/dist/collections/engine-support.es.js +35 -0
- package/dist/collections/engine-support.es.js.map +1 -0
- package/dist/collections/field-access.d.ts +13 -0
- package/dist/collections/field-access.es.js +142 -0
- package/dist/collections/field-access.es.js.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/soft-delete.es.js +34 -0
- package/dist/collections/soft-delete.es.js.map +1 -0
- package/dist/collections/unknown-field.d.ts +65 -0
- package/dist/collections/unknown-field.es.js +135 -0
- package/dist/collections/unknown-field.es.js.map +1 -0
- package/dist/data/buildRebaseData.d.ts +27 -2
- package/dist/data/buildRebaseData.es.js +1026 -0
- package/dist/data/buildRebaseData.es.js.map +1 -0
- package/dist/data/buildRoutedRebaseData.d.ts +6 -1
- package/dist/data/buildRoutedRebaseData.es.js +47 -0
- package/dist/data/buildRoutedRebaseData.es.js.map +1 -0
- package/dist/data/collection-accessor.d.ts +54 -0
- package/dist/data/collection-accessor.es.js +101 -0
- package/dist/data/collection-accessor.es.js.map +1 -0
- package/dist/data/cursor.d.ts +1 -1
- package/dist/data/cursor.es.js +166 -0
- package/dist/data/cursor.es.js.map +1 -0
- package/dist/data/filter-conditions.es.js +24 -0
- package/dist/data/filter-conditions.es.js.map +1 -0
- package/dist/data/filter-dialect.d.ts +3 -3
- package/dist/data/filter-dialect.es.js +786 -0
- package/dist/data/filter-dialect.es.js.map +1 -0
- package/dist/data/include-spec.d.ts +2 -0
- package/dist/data/include-spec.es.js +273 -0
- package/dist/data/include-spec.es.js.map +1 -0
- package/dist/data/paginate.d.ts +8 -1
- package/dist/data/paginate.es.js +181 -0
- package/dist/data/paginate.es.js.map +1 -0
- package/dist/data/query_builder.d.ts +52 -4
- package/dist/data/query_builder.es.js +151 -0
- package/dist/data/query_builder.es.js.map +1 -0
- package/dist/data/resolveDataSource.es.js +87 -0
- package/dist/data/resolveDataSource.es.js.map +1 -0
- package/dist/data/row-etag.d.ts +46 -0
- package/dist/data/row-etag.es.js +64 -0
- package/dist/data/row-etag.es.js.map +1 -0
- package/dist/data/sort-dialect.es.js +236 -0
- package/dist/data/sort-dialect.es.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +56 -7235
- package/dist/table-classification.es.js +107 -0
- package/dist/table-classification.es.js.map +1 -0
- package/dist/util/auth-default-policies.es.js +184 -0
- package/dist/util/auth-default-policies.es.js.map +1 -0
- package/dist/util/builders.es.js +15 -0
- package/dist/util/builders.es.js.map +1 -0
- package/dist/util/callback-errors.es.js +164 -0
- package/dist/util/callback-errors.es.js.map +1 -0
- package/dist/util/callbacks.es.js +72 -0
- package/dist/util/callbacks.es.js.map +1 -0
- package/dist/util/collections.es.js +79 -0
- package/dist/util/collections.es.js.map +1 -0
- package/dist/util/common.es.js +7 -0
- package/dist/util/common.es.js.map +1 -0
- package/dist/util/conditions.es.js +100 -0
- package/dist/util/conditions.es.js.map +1 -0
- package/dist/util/copy.d.ts +33 -0
- package/dist/util/copy.es.js +74 -0
- package/dist/util/copy.es.js.map +1 -0
- package/dist/util/download-url-cache.d.ts +122 -0
- package/dist/util/download-url-cache.es.js +187 -0
- package/dist/util/download-url-cache.es.js.map +1 -0
- package/dist/util/email.es.js +36 -0
- package/dist/util/email.es.js.map +1 -0
- package/dist/util/entities.d.ts +27 -2
- package/dist/util/entities.es.js +328 -0
- package/dist/util/entities.es.js.map +1 -0
- package/dist/util/enums.d.ts +10 -1
- package/dist/util/enums.es.js +37 -0
- package/dist/util/enums.es.js.map +1 -0
- package/dist/util/identity.es.js +138 -0
- package/dist/util/identity.es.js.map +1 -0
- package/dist/util/index.d.ts +6 -0
- package/dist/util/internal-tables.d.ts +10 -0
- package/dist/util/internal-tables.es.js +202 -0
- package/dist/util/internal-tables.es.js.map +1 -0
- package/dist/util/junction-policies.es.js +273 -0
- package/dist/util/junction-policies.es.js.map +1 -0
- package/dist/util/paths.es.js +26 -0
- package/dist/util/paths.es.js.map +1 -0
- package/dist/util/permissions.es.js +108 -0
- package/dist/util/permissions.es.js.map +1 -0
- package/dist/util/pg-column-to-property.es.js +303 -0
- package/dist/util/pg-column-to-property.es.js.map +1 -0
- package/dist/util/policy/compileRulePolicies.d.ts +32 -0
- package/dist/util/policy/compileRulePolicies.es.js +43 -0
- package/dist/util/policy/compileRulePolicies.es.js.map +1 -0
- package/dist/util/policy/evaluatePolicy.es.js +104 -0
- package/dist/util/policy/evaluatePolicy.es.js.map +1 -0
- package/dist/util/policy/index.d.ts +1 -0
- package/dist/util/policy/policyToPostgres.d.ts +24 -0
- package/dist/util/policy/policyToPostgres.es.js +593 -0
- package/dist/util/policy/policyToPostgres.es.js.map +1 -0
- package/dist/util/policy/policyToSecurityRule.es.js +79 -0
- package/dist/util/policy/policyToSecurityRule.es.js.map +1 -0
- package/dist/util/policy/securityRuleToConditions.es.js +48 -0
- package/dist/util/policy/securityRuleToConditions.es.js.map +1 -0
- package/dist/util/policy/sqlToPolicy.es.js +326 -0
- package/dist/util/policy/sqlToPolicy.es.js.map +1 -0
- package/dist/util/relations.es.js +243 -0
- package/dist/util/relations.es.js.map +1 -0
- package/dist/util/resolutions.es.js +318 -0
- package/dist/util/resolutions.es.js.map +1 -0
- package/dist/util/resolve-relation.es.js +163 -0
- package/dist/util/resolve-relation.es.js.map +1 -0
- package/dist/util/resumable-upload-threshold.d.ts +15 -0
- package/dist/util/resumable-upload-threshold.es.js +20 -0
- package/dist/util/resumable-upload-threshold.es.js.map +1 -0
- package/dist/util/resumable-upload.d.ts +51 -0
- package/dist/util/resumable-upload.es.js +121 -0
- package/dist/util/resumable-upload.es.js.map +1 -0
- package/dist/util/sql-redaction.d.ts +21 -0
- package/dist/util/sql-redaction.es.js +107 -0
- package/dist/util/sql-redaction.es.js.map +1 -0
- package/dist/util/sql-rows.es.js +41 -0
- package/dist/util/sql-rows.es.js.map +1 -0
- package/dist/util/storage.d.ts +26 -4
- package/dist/util/storage.es.js +117 -0
- package/dist/util/storage.es.js.map +1 -0
- package/dist/util/string-column-length.es.js +31 -0
- package/dist/util/string-column-length.es.js.map +1 -0
- package/dist/util/tenant.es.js +201 -0
- package/dist/util/tenant.es.js.map +1 -0
- package/dist/util/untrusted-envelope.d.ts +25 -0
- package/dist/util/untrusted-envelope.es.js +40 -0
- package/dist/util/untrusted-envelope.es.js.map +1 -0
- package/package.json +5 -3
- package/dist/index.es.js.map +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { normalizeOrderBy } from "./sort-dialect.es.js";
|
|
2
|
+
import { DEFAULT_LIST_LIMIT, RebaseClientError } from "@rebasepro/types";
|
|
3
|
+
//#region src/data/paginate.ts
|
|
4
|
+
/**
|
|
5
|
+
* The pagination engine behind `iterate()` / `findAll()`.
|
|
6
|
+
*
|
|
7
|
+
* It lives here, above both transports, on purpose: the HTTP client and the
|
|
8
|
+
* in-process accessor implement the same `SDKCollectionClient` contract, and a
|
|
9
|
+
* helper written twice is a helper that drifts. Both call into this file, so
|
|
10
|
+
* "the SDK paginates like *this*" has exactly one definition.
|
|
11
|
+
*
|
|
12
|
+
* Everything below is expressed in terms of a single `find(params)` function,
|
|
13
|
+
* which is all either transport has to supply.
|
|
14
|
+
*/
|
|
15
|
+
/** Rows requested per page when the caller does not say. */
|
|
16
|
+
var DEFAULT_PAGE_SIZE = 200;
|
|
17
|
+
/** Rows `findAll()` will materialise before it refuses to continue. */
|
|
18
|
+
var DEFAULT_FIND_ALL_MAX_ROWS = 1e4;
|
|
19
|
+
/**
|
|
20
|
+
* Requests one walk may make before it gives up on the server ever saying
|
|
21
|
+
* `hasMore: false`. At the default page size that is two million rows — far
|
|
22
|
+
* past any legitimate walk, and short of running forever.
|
|
23
|
+
*/
|
|
24
|
+
var DEFAULT_MAX_PAGES = 1e4;
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when a walk stops for a reason the caller needs to know about.
|
|
27
|
+
*
|
|
28
|
+
* Every one of these is a case where the alternative would be silent: a
|
|
29
|
+
* truncated array that looks complete, or a loop that never returns. Check
|
|
30
|
+
* {@link code} to tell them apart.
|
|
31
|
+
*/
|
|
32
|
+
var RebasePaginationError = class RebasePaginationError extends Error {
|
|
33
|
+
code;
|
|
34
|
+
constructor(code, message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "RebasePaginationError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* What a read naming both `page` and `offset` is refused with — by the server
|
|
43
|
+
* (400 `PAGE_WITH_OFFSET`) and, before any request, by every client.
|
|
44
|
+
*/
|
|
45
|
+
var PAGE_WITH_OFFSET_MESSAGE = "`page` and `offset` describe the same window; send one.";
|
|
46
|
+
/**
|
|
47
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
48
|
+
*
|
|
49
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
50
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
51
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
52
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
53
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
54
|
+
* rows are the mildest of those outcomes.
|
|
55
|
+
*
|
|
56
|
+
* `page` and `offset` together are refused with `PAGE_WITH_OFFSET`, as the
|
|
57
|
+
* server refuses them: they name one window twice, and `page` used to win
|
|
58
|
+
* silently over an `offset` the caller then never saw applied. `driverOffset`
|
|
59
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
60
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
61
|
+
* look like it is paging by offset.
|
|
62
|
+
*/
|
|
63
|
+
function resolveFindWindow(params) {
|
|
64
|
+
if (params?.page != null && params.offset != null) throw new RebaseClientError(PAGE_WITH_OFFSET_MESSAGE, { code: "PAGE_WITH_OFFSET" });
|
|
65
|
+
const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
|
|
66
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
67
|
+
return {
|
|
68
|
+
limit,
|
|
69
|
+
offset,
|
|
70
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function normalizePageSize(raw) {
|
|
74
|
+
if (raw === void 0 || !Number.isFinite(raw)) return DEFAULT_PAGE_SIZE;
|
|
75
|
+
return Math.max(1, Math.floor(raw));
|
|
76
|
+
}
|
|
77
|
+
function normalizeMaxPages(raw) {
|
|
78
|
+
if (raw === void 0) return DEFAULT_MAX_PAGES;
|
|
79
|
+
if (raw === Number.POSITIVE_INFINITY) return raw;
|
|
80
|
+
if (!Number.isFinite(raw)) return DEFAULT_MAX_PAGES;
|
|
81
|
+
return Math.max(1, Math.floor(raw));
|
|
82
|
+
}
|
|
83
|
+
function normalizeMaxRows(raw) {
|
|
84
|
+
if (raw === void 0) return DEFAULT_FIND_ALL_MAX_ROWS;
|
|
85
|
+
if (raw === Number.POSITIVE_INFINITY) return raw;
|
|
86
|
+
if (!Number.isFinite(raw)) return DEFAULT_FIND_ALL_MAX_ROWS;
|
|
87
|
+
return Math.max(0, Math.floor(raw));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Walk every row a query matches, yielding one row at a time and fetching the
|
|
91
|
+
* next page only when the consumer asks for it.
|
|
92
|
+
*
|
|
93
|
+
* See {@link SDKCollectionClient.iterate} for the caller-facing contract,
|
|
94
|
+
* including the offset-drift caveat and the `cursor` alternative.
|
|
95
|
+
*
|
|
96
|
+
* @param find the transport's single-page read
|
|
97
|
+
* @param params `find()` parameters minus the window, plus the walk options
|
|
98
|
+
* @param label the collection name, so an error says which walk failed
|
|
99
|
+
*/
|
|
100
|
+
async function* paginateFind(find, params, label = "collection") {
|
|
101
|
+
const { pageSize, cursor, maxPages, after: startAfter, offset: startOffset, page: startPage, ...rest } = params ?? {};
|
|
102
|
+
const findParams = {
|
|
103
|
+
...rest,
|
|
104
|
+
count: "none"
|
|
105
|
+
};
|
|
106
|
+
const size = normalizePageSize(pageSize);
|
|
107
|
+
const pageCap = normalizeMaxPages(maxPages);
|
|
108
|
+
const seekRequested = cursor !== void 0 && cursor !== null || !!startAfter;
|
|
109
|
+
if (cursor !== void 0 && cursor !== null) {
|
|
110
|
+
const field = typeof cursor === "string" ? cursor : cursor.field;
|
|
111
|
+
const requested = typeof cursor === "object" && cursor !== null ? cursor.direction : void 0;
|
|
112
|
+
if (!normalizeOrderBy(findParams.orderBy)) findParams.orderBy = [field, requested ?? "asc"];
|
|
113
|
+
}
|
|
114
|
+
let offset = resolveFindWindow({
|
|
115
|
+
limit: size,
|
|
116
|
+
offset: startOffset,
|
|
117
|
+
page: startPage
|
|
118
|
+
}).offset;
|
|
119
|
+
let pages = 0;
|
|
120
|
+
let after = startAfter || void 0;
|
|
121
|
+
let seekingByServerCursor = false;
|
|
122
|
+
for (;;) {
|
|
123
|
+
if (pages >= pageCap) throw new RebasePaginationError("max-pages", `Iterating "${label}" made ${pages} requests without the server reporting the end of the collection. Stopping rather than looping forever — raise \`maxPages\` if the walk is genuinely this long, or check that the backend sets \`meta.hasMore\`.`);
|
|
124
|
+
const pageParams = {
|
|
125
|
+
...findParams,
|
|
126
|
+
limit: size
|
|
127
|
+
};
|
|
128
|
+
if (seekRequested) {
|
|
129
|
+
if (after) pageParams.after = after;
|
|
130
|
+
} else if (seekingByServerCursor && after) pageParams.after = after;
|
|
131
|
+
else pageParams.offset = offset;
|
|
132
|
+
const page = await find(pageParams);
|
|
133
|
+
pages += 1;
|
|
134
|
+
const rows = page?.data ?? [];
|
|
135
|
+
if (rows.length === 0) return;
|
|
136
|
+
for (const row of rows) yield row;
|
|
137
|
+
if (page?.meta?.hasMore !== true) return;
|
|
138
|
+
if (seekRequested) {
|
|
139
|
+
const next = page.meta.nextCursor;
|
|
140
|
+
if (!next) throw new RebasePaginationError("cursor-missing", `Cannot seek past the last row of "${label}": the server reported another page but issued no cursor for it. An ordering with no stored value to compare against — relevance (\`_score\`) — cannot key a cursor. Drop \`cursor\` to page by offset.`);
|
|
141
|
+
if (next === after) throw new RebasePaginationError("cursor-stalled", `Iterating "${label}" is stuck: two pages in a row ended on the same cursor, so the walk cannot advance. Continuing would loop forever. Page by offset instead, or report this — a cursor that does not move is a server-side bug.`);
|
|
142
|
+
after = next;
|
|
143
|
+
} else {
|
|
144
|
+
offset += rows.length;
|
|
145
|
+
const next = page.meta.nextCursor;
|
|
146
|
+
seekingByServerCursor = !!next && next !== after;
|
|
147
|
+
after = seekingByServerCursor ? next : void 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* {@link paginateFind}, collected into an array under a ceiling.
|
|
153
|
+
*
|
|
154
|
+
* See {@link SDKCollectionClient.findAll}.
|
|
155
|
+
*/
|
|
156
|
+
async function collectAllPages(find, params, label = "collection") {
|
|
157
|
+
const { maxRows, ...rest } = params ?? {};
|
|
158
|
+
const cap = normalizeMaxRows(maxRows);
|
|
159
|
+
const out = [];
|
|
160
|
+
for await (const row of paginateFind(find, rest, label)) {
|
|
161
|
+
out.push(row);
|
|
162
|
+
if (out.length > cap) throw new RebasePaginationError("max-rows", `findAll("${label}") matched more than ${cap} rows. Returning the first ${cap} would look like the whole answer and quietly not be one, so this throws instead. Raise \`maxRows\` if you meant to load them all, or stream with \`iterate()\`.`);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build the `iterate` / `findAll` pair for one collection from its `find`.
|
|
168
|
+
*
|
|
169
|
+
* Both transports call this, which is what keeps the two implementations from
|
|
170
|
+
* being two implementations.
|
|
171
|
+
*/
|
|
172
|
+
function createPaginationHelpers(find, label) {
|
|
173
|
+
return {
|
|
174
|
+
iterate: (params) => paginateFind(find, params, label),
|
|
175
|
+
findAll: (params) => collectAllPages(find, params, label)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_PAGE_SIZE, PAGE_WITH_OFFSET_MESSAGE, RebasePaginationError, collectAllPages, createPaginationHelpers, paginateFind, resolveFindWindow };
|
|
180
|
+
|
|
181
|
+
//# sourceMappingURL=paginate.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paginate.es.js","names":[],"sources":["../../src/data/paginate.ts"],"sourcesContent":["import {\n DEFAULT_LIST_LIMIT,\n FindAllParams,\n FindParams,\n FindResult,\n IterateParams,\n RebaseClientError\n} from \"@rebasepro/types\";\nimport { normalizeOrderBy } from \"./sort-dialect\";\n\n/**\n * The pagination engine behind `iterate()` / `findAll()`.\n *\n * It lives here, above both transports, on purpose: the HTTP client and the\n * in-process accessor implement the same `SDKCollectionClient` contract, and a\n * helper written twice is a helper that drifts. Both call into this file, so\n * \"the SDK paginates like *this*\" has exactly one definition.\n *\n * Everything below is expressed in terms of a single `find(params)` function,\n * which is all either transport has to supply.\n */\n\n/** Rows requested per page when the caller does not say. */\nexport const DEFAULT_PAGE_SIZE = 200;\n\n/** Rows `findAll()` will materialise before it refuses to continue. */\nexport const DEFAULT_FIND_ALL_MAX_ROWS = 10_000;\n\n/**\n * Requests one walk may make before it gives up on the server ever saying\n * `hasMore: false`. At the default page size that is two million rows — far\n * past any legitimate walk, and short of running forever.\n */\nexport const DEFAULT_MAX_PAGES = 10_000;\n\n/** Why a pagination walk refused to continue. */\nexport type PaginationErrorCode =\n /** `findAll()` matched more rows than its ceiling allows. */\n | \"max-rows\"\n /** The walk made its maximum number of requests without the server finishing. */\n | \"max-pages\"\n /**\n * The server said there was another page but issued no cursor to reach it.\n *\n * A query whose ordering has no stored value to seek on — relevance — is the\n * case that produces this. Page it by offset instead.\n */\n | \"cursor-missing\"\n /** Two consecutive pages returned the same cursor, so the walk cannot advance. */\n | \"cursor-stalled\";\n\n/**\n * Thrown when a walk stops for a reason the caller needs to know about.\n *\n * Every one of these is a case where the alternative would be silent: a\n * truncated array that looks complete, or a loop that never returns. Check\n * {@link code} to tell them apart.\n */\nexport class RebasePaginationError extends Error {\n readonly code: PaginationErrorCode;\n\n constructor(code: PaginationErrorCode, message: string) {\n super(message);\n this.name = \"RebasePaginationError\";\n this.code = code;\n // Keeps `instanceof` working when this is compiled down for an older\n // target, where extending a builtin otherwise loses the prototype.\n Object.setPrototypeOf(this, RebasePaginationError.prototype);\n }\n}\n\n/**\n * What a read naming both `page` and `offset` is refused with — by the server\n * (400 `PAGE_WITH_OFFSET`) and, before any request, by every client.\n */\nexport const PAGE_WITH_OFFSET_MESSAGE = \"`page` and `offset` describe the same window; send one.\";\n\n/** The one thing a transport has to provide to be paginated. */\nexport type PageFinder<M extends Record<string, unknown> = Record<string, unknown>> =\n (params: FindParams<M>) => Promise<FindResult<M>>;\n\n/**\n * Resolve `limit`/`offset`/`page` into the window a read will actually use.\n *\n * Lives here, next to the walk, for the reason at the top of this file: every\n * transport has to mean the same thing by \"page two\". Four of them did not —\n * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first\n * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and\n * the published type documented a fourth number. Pages that overlap or skip\n * rows are the mildest of those outcomes.\n *\n * `page` and `offset` together are refused with `PAGE_WITH_OFFSET`, as the\n * server refuses them: they name one window twice, and `page` used to win\n * silently over an `offset` the caller then never saw applied. `driverOffset`\n * is the value to hand a driver: it stays `undefined` when the caller named no\n * offset, because keyset pagination seeks with a `where` clause and must not\n * look like it is paging by offset.\n */\nexport function resolveFindWindow(\n params?: Pick<FindParams, \"limit\" | \"offset\" | \"page\">\n): { limit: number; offset: number; driverOffset: number | undefined } {\n if (params?.page != null && params.offset != null) {\n throw new RebaseClientError(PAGE_WITH_OFFSET_MESSAGE, { code: \"PAGE_WITH_OFFSET\" });\n }\n const limit = params?.limit ?? DEFAULT_LIST_LIMIT;\n const offset = params?.page != null\n ? Math.max(0, (params.page - 1) * limit)\n : (params?.offset ?? 0);\n return {\n limit,\n offset,\n driverOffset: params?.page != null ? offset : params?.offset\n };\n}\n\nfunction normalizePageSize(raw: number | undefined): number {\n if (raw === undefined || !Number.isFinite(raw)) return DEFAULT_PAGE_SIZE;\n return Math.max(1, Math.floor(raw));\n}\n\nfunction normalizeMaxPages(raw: number | undefined): number {\n if (raw === undefined) return DEFAULT_MAX_PAGES;\n if (raw === Number.POSITIVE_INFINITY) return raw;\n if (!Number.isFinite(raw)) return DEFAULT_MAX_PAGES;\n return Math.max(1, Math.floor(raw));\n}\n\nfunction normalizeMaxRows(raw: number | undefined): number {\n if (raw === undefined) return DEFAULT_FIND_ALL_MAX_ROWS;\n if (raw === Number.POSITIVE_INFINITY) return raw;\n if (!Number.isFinite(raw)) return DEFAULT_FIND_ALL_MAX_ROWS;\n return Math.max(0, Math.floor(raw));\n}\n\n/**\n * Walk every row a query matches, yielding one row at a time and fetching the\n * next page only when the consumer asks for it.\n *\n * See {@link SDKCollectionClient.iterate} for the caller-facing contract,\n * including the offset-drift caveat and the `cursor` alternative.\n *\n * @param find the transport's single-page read\n * @param params `find()` parameters minus the window, plus the walk options\n * @param label the collection name, so an error says which walk failed\n */\nexport async function* paginateFind<M extends Record<string, unknown> = Record<string, unknown>>(\n find: PageFinder<M>,\n params?: IterateParams<M>,\n label = \"collection\"\n): AsyncGenerator<M, void, undefined> {\n const {\n pageSize,\n cursor,\n maxPages,\n // The window is the walk's, but where it starts is the caller's. Left\n // in `rest`, these rode along on every page beside the walk's own: a\n // caller's `after` went out next to `offset=0`, which the server\n // refuses outright (CURSOR_WITH_OFFSET), and a caller's `offset` was\n // overwritten by that `0`, so the walk silently started over from the\n // top. `IterateParams` omits `offset` and `page`, but the query builder\n // hands its whole state to `iterate()`, `.offset(40)` included.\n after: startAfter,\n offset: startOffset,\n page: startPage,\n ...rest\n } = (params ?? {}) as IterateParams<M> & Pick<FindParams<M>, \"offset\" | \"page\">;\n\n // No page of a walk is counted. Nothing it returns carries a total —\n // `iterate()` yields rows, `findAll()` an array — and `hasMore`, the one\n // thing it reads off `meta`, does not need one: without a count the read\n // asks for one row past the page. The count is most of what a list read\n // costs on a large table (1.8 s of a 100k-row walk's SQL, 26 ms without).\n // A caller's own `count` is overridden for the same reason.\n const findParams = { ...rest, count: \"none\" } as FindParams<M>;\n const size = normalizePageSize(pageSize);\n const pageCap = normalizeMaxPages(maxPages);\n\n // ── Cursor (keyset) setup ────────────────────────────────────────────────\n //\n // The walk no longer builds a keyset of its own. It used to: a `>`/`<` on\n // one column, expressed as an extra `where`, which threw on any multi-key\n // sort (\"keyset pagination advances along a single column\") and dropped\n // every row whose sort value was NULL, because `> value` answers *unknown*\n // against NULL. The driver has had a NULL-correct multi-key comparison all\n // along and nothing over HTTP could reach it.\n //\n // So this is now a *request* for seeking, not an implementation of it: the\n // server issues `meta.nextCursor` and the walk hands it back as `after`.\n // Multi-key sorts and nullable keys work because the comparison is the\n // driver's, and there is one of it.\n //\n // A caller's own `after` is a request for seeking too: it is a cursor\n // issued for this query's sort, so the walk continues from it by the same\n // handoff.\n const seekRequested = (cursor !== undefined && cursor !== null) || !!startAfter;\n if (cursor !== undefined && cursor !== null) {\n // A named column still means \"sort by this and seek along it\", which is\n // what every existing caller wrote. It is an `orderBy` now rather than\n // a second pagination mode — the seeking itself needs no column named,\n // since the cursor carries whatever keys the sort used.\n const field = typeof cursor === \"string\" ? cursor : cursor.field;\n const requested = (typeof cursor === \"object\" && cursor !== null) ? cursor.direction : undefined;\n const explicit = normalizeOrderBy(findParams.orderBy);\n // An explicit `orderBy` wins and the named column is redundant, not\n // wrong: seeking follows whatever the query is sorted by, so there is\n // no longer a mismatch to refuse.\n if (!explicit) {\n findParams.orderBy = [field, requested ?? \"asc\"] as FindParams<M>[\"orderBy\"];\n }\n }\n\n // `page` counts in pages of this walk's size, by the one rule every read\n // resolves it with.\n let offset = resolveFindWindow({ limit: size, offset: startOffset, page: startPage }).offset;\n let pages = 0;\n let after: string | undefined = startAfter || undefined;\n\n // ── The default walk seeks too, whenever the server lets it ─────────────\n //\n // A walk nobody asked to seek still continues from `meta.nextCursor` when a\n // page carries one. Advancing `offset` instead made every request skip\n // `offset` rows in the database before reading the next page, so the walk\n // cost grew with the square of the collection: 4.5 s of SQL for 100k rows\n // where seeking is 1.8 s — and the order is the same, because the cursor\n // is the server's, built over the very sort the offset pages used.\n //\n // Where no cursor comes — relevance, a vector search, a distinct read, a\n // server that predates cursors — it pages by offset exactly as before, from\n // the position it has reached (`offset` keeps counting rows either way).\n // `after` and `offset` never go out together: the server refuses that\n // (`CURSOR_WITH_OFFSET`). Only an explicit `cursor`/`after` walk treats a\n // missing or stalled cursor as an error; this one never promised to seek.\n let seekingByServerCursor = false;\n\n for (;;) {\n if (pages >= pageCap) {\n throw new RebasePaginationError(\n \"max-pages\",\n `Iterating \"${label}\" made ${pages} requests without the server reporting the end of ` +\n `the collection. Stopping rather than looping forever — raise \\`maxPages\\` if the walk ` +\n `is genuinely this long, or check that the backend sets \\`meta.hasMore\\`.`\n );\n }\n\n const pageParams: FindParams<M> = { ...findParams, limit: size };\n if (seekRequested) {\n if (after) pageParams.after = after;\n } else if (seekingByServerCursor && after) {\n pageParams.after = after;\n } else {\n pageParams.offset = offset;\n }\n\n const page = await find(pageParams);\n pages += 1;\n\n const rows = page?.data ?? [];\n // A page with nothing on it always ends the walk, whatever the server\n // claims about `hasMore` — there is no cursor to advance and no offset\n // that would ever move past it.\n if (rows.length === 0) return;\n\n for (const row of rows) {\n yield row;\n }\n\n // The server is the only authority on whether more rows exist. Never\n // infer it from `rows.length >= size`: a last page that happens to be\n // exactly full is indistinguishable from a middle one, and guessing\n // there drops every row after it.\n if (page?.meta?.hasMore !== true) return;\n\n if (seekRequested) {\n const next = page.meta.nextCursor;\n if (!next) {\n throw new RebasePaginationError(\n \"cursor-missing\",\n `Cannot seek past the last row of \"${label}\": the server reported another page but ` +\n `issued no cursor for it. An ordering with no stored value to compare against — ` +\n `relevance (\\`_score\\`) — cannot key a cursor. Drop \\`cursor\\` to page by offset.`\n );\n }\n if (next === after) {\n throw new RebasePaginationError(\n \"cursor-stalled\",\n `Iterating \"${label}\" is stuck: two pages in a row ended on the same cursor, so the ` +\n `walk cannot advance. Continuing would loop forever. Page by offset instead, or ` +\n `report this — a cursor that does not move is a server-side bug.`\n );\n }\n after = next;\n } else {\n // Advance by what actually arrived, not by the page size: a server\n // free to return fewer rows than asked for would otherwise leave a\n // hole in the walk. Counted on seeked pages too, so a fallback to\n // offset resumes exactly where the walk has got to.\n offset += rows.length;\n const next = page.meta.nextCursor;\n // A cursor equal to the one just sent cannot advance the walk;\n // offset still can.\n seekingByServerCursor = !!next && next !== after;\n after = seekingByServerCursor ? next : undefined;\n }\n }\n}\n\n/**\n * {@link paginateFind}, collected into an array under a ceiling.\n *\n * See {@link SDKCollectionClient.findAll}.\n */\nexport async function collectAllPages<M extends Record<string, unknown> = Record<string, unknown>>(\n find: PageFinder<M>,\n params?: FindAllParams<M>,\n label = \"collection\"\n): Promise<M[]> {\n const { maxRows, ...rest } = (params ?? {}) as FindAllParams<M> & Record<string, unknown>;\n const cap = normalizeMaxRows(maxRows as number | undefined);\n\n const out: M[] = [];\n for await (const row of paginateFind<M>(find, rest as IterateParams<M>, label)) {\n out.push(row);\n if (out.length > cap) {\n throw new RebasePaginationError(\n \"max-rows\",\n `findAll(\"${label}\") matched more than ${cap} rows. Returning the first ${cap} would ` +\n `look like the whole answer and quietly not be one, so this throws instead. Raise ` +\n `\\`maxRows\\` if you meant to load them all, or stream with \\`iterate()\\`.`\n );\n }\n }\n return out;\n}\n\n/**\n * Build the `iterate` / `findAll` pair for one collection from its `find`.\n *\n * Both transports call this, which is what keeps the two implementations from\n * being two implementations.\n */\nexport function createPaginationHelpers<M extends Record<string, unknown> = Record<string, unknown>>(\n find: PageFinder<M>,\n label: string\n): {\n iterate: (params?: IterateParams<M>) => AsyncIterableIterator<M>;\n findAll: (params?: FindAllParams<M>) => Promise<M[]>;\n} {\n return {\n iterate: (params?: IterateParams<M>) => paginateFind<M>(find, params, label),\n findAll: (params?: FindAllParams<M>) => collectAllPages<M>(find, params, label)\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,IAAa,oBAAoB;;AAGjC,IAAa,4BAA4B;;;;;;AAOzC,IAAa,oBAAoB;;;;;;;;AAyBjC,IAAa,wBAAb,MAAa,8BAA8B,MAAM;CAC7C;CAEA,YAAY,MAA2B,SAAiB;EACpD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EAGZ,OAAO,eAAe,MAAM,sBAAsB,SAAS;CAC/D;AACJ;;;;;AAMA,IAAa,2BAA2B;;;;;;;;;;;;;;;;;;AAuBxC,SAAgB,kBACZ,QACmE;CACnE,IAAI,QAAQ,QAAQ,QAAQ,OAAO,UAAU,MACzC,MAAM,IAAI,kBAAkB,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;CAEtF,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,SAAS,QAAQ,QAAQ,OACzB,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,KAAK,IACpC,QAAQ,UAAU;CACzB,OAAO;EACH;EACA;EACA,cAAc,QAAQ,QAAQ,OAAO,SAAS,QAAQ;CAC1D;AACJ;AAEA,SAAS,kBAAkB,KAAiC;CACxD,IAAI,QAAQ,KAAA,KAAa,CAAC,OAAO,SAAS,GAAG,GAAG,OAAO;CACvD,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC;AACtC;AAEA,SAAS,kBAAkB,KAAiC;CACxD,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,IAAI,QAAQ,OAAO,mBAAmB,OAAO;CAC7C,IAAI,CAAC,OAAO,SAAS,GAAG,GAAG,OAAO;CAClC,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC;AACtC;AAEA,SAAS,iBAAiB,KAAiC;CACvD,IAAI,QAAQ,KAAA,GAAW,OAAO;CAC9B,IAAI,QAAQ,OAAO,mBAAmB,OAAO;CAC7C,IAAI,CAAC,OAAO,SAAS,GAAG,GAAG,OAAO;CAClC,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC;AACtC;;;;;;;;;;;;AAaA,gBAAuB,aACnB,MACA,QACA,QAAQ,cAC0B;CAClC,MAAM,EACF,UACA,QACA,UAQA,OAAO,YACP,QAAQ,aACR,MAAM,WACN,GAAG,SACF,UAAU,CAAC;CAQhB,MAAM,aAAa;EAAE,GAAG;EAAM,OAAO;CAAO;CAC5C,MAAM,OAAO,kBAAkB,QAAQ;CACvC,MAAM,UAAU,kBAAkB,QAAQ;CAmB1C,MAAM,gBAAiB,WAAW,KAAA,KAAa,WAAW,QAAS,CAAC,CAAC;CACrE,IAAI,WAAW,KAAA,KAAa,WAAW,MAAM;EAKzC,MAAM,QAAQ,OAAO,WAAW,WAAW,SAAS,OAAO;EAC3D,MAAM,YAAa,OAAO,WAAW,YAAY,WAAW,OAAQ,OAAO,YAAY,KAAA;EAKvF,IAAI,CAJa,iBAAiB,WAAW,OAIxC,GACD,WAAW,UAAU,CAAC,OAAO,aAAa,KAAK;CAEvD;CAIA,IAAI,SAAS,kBAAkB;EAAE,OAAO;EAAM,QAAQ;EAAa,MAAM;CAAU,CAAC,CAAC,CAAC;CACtF,IAAI,QAAQ;CACZ,IAAI,QAA4B,cAAc,KAAA;CAiB9C,IAAI,wBAAwB;CAE5B,SAAS;EACL,IAAI,SAAS,SACT,MAAM,IAAI,sBACN,aACA,cAAc,MAAM,SAAS,MAAM,iNAGvC;EAGJ,MAAM,aAA4B;GAAE,GAAG;GAAY,OAAO;EAAK;EAC/D,IAAI;OACI,OAAO,WAAW,QAAQ;EAAA,OAC3B,IAAI,yBAAyB,OAChC,WAAW,QAAQ;OAEnB,WAAW,SAAS;EAGxB,MAAM,OAAO,MAAM,KAAK,UAAU;EAClC,SAAS;EAET,MAAM,OAAO,MAAM,QAAQ,CAAC;EAI5B,IAAI,KAAK,WAAW,GAAG;EAEvB,KAAK,MAAM,OAAO,MACd,MAAM;EAOV,IAAI,MAAM,MAAM,YAAY,MAAM;EAElC,IAAI,eAAe;GACf,MAAM,OAAO,KAAK,KAAK;GACvB,IAAI,CAAC,MACD,MAAM,IAAI,sBACN,kBACA,qCAAqC,MAAM,wMAG/C;GAEJ,IAAI,SAAS,OACT,MAAM,IAAI,sBACN,kBACA,cAAc,MAAM,+MAGxB;GAEJ,QAAQ;EACZ,OAAO;GAKH,UAAU,KAAK;GACf,MAAM,OAAO,KAAK,KAAK;GAGvB,wBAAwB,CAAC,CAAC,QAAQ,SAAS;GAC3C,QAAQ,wBAAwB,OAAO,KAAA;EAC3C;CACJ;AACJ;;;;;;AAOA,eAAsB,gBAClB,MACA,QACA,QAAQ,cACI;CACZ,MAAM,EAAE,SAAS,GAAG,SAAU,UAAU,CAAC;CACzC,MAAM,MAAM,iBAAiB,OAA6B;CAE1D,MAAM,MAAW,CAAC;CAClB,WAAW,MAAM,OAAO,aAAgB,MAAM,MAA0B,KAAK,GAAG;EAC5E,IAAI,KAAK,GAAG;EACZ,IAAI,IAAI,SAAS,KACb,MAAM,IAAI,sBACN,YACA,YAAY,MAAM,uBAAuB,IAAI,6BAA6B,IAAI,iKAGlF;CAER;CACA,OAAO;AACX;;;;;;;AAQA,SAAgB,wBACZ,MACA,OAIF;CACE,OAAO;EACH,UAAU,WAA8B,aAAgB,MAAM,QAAQ,KAAK;EAC3E,UAAU,WAA8B,gBAAmB,MAAM,QAAQ,KAAK;CAClF;AACJ"}
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { CollectionAccessor, FilterCondition, FindResponse, LogicalCondition, QueryBuilderInterface, WhereFilterOp, WhereValueFor, type ComputedSortField } from "@rebasepro/types";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Any condition a group may hold. The helpers below are generic over it so the
|
|
4
|
+
* group they return keeps each condition's column, operator and value exactly
|
|
5
|
+
* as written — which is what lets a typed `logical` / `.where(group)` check
|
|
6
|
+
* them against the row ({@link LogicalConditionFor}). Their results are still
|
|
7
|
+
* plain `LogicalCondition`s anywhere one is expected.
|
|
8
|
+
*/
|
|
9
|
+
type AnyCondition = FilterCondition | LogicalCondition;
|
|
10
|
+
/** A value as `cond` keeps it: literals stay literals, so `"draft"` can match a `"draft" | "published"` column. */
|
|
11
|
+
type ConditionValue = string | number | bigint | boolean | symbol | object | null | undefined;
|
|
12
|
+
/** An empty group, to push conditions into later. */
|
|
13
|
+
export declare function or(): {
|
|
14
|
+
type: "or";
|
|
15
|
+
conditions: AnyCondition[];
|
|
16
|
+
};
|
|
17
|
+
/** The group keeps the union of what was passed, so a mixed `or(cond(…), and(…))` keeps each one. */
|
|
18
|
+
export declare function or<Cs extends AnyCondition[]>(...conditions: Cs): {
|
|
19
|
+
type: "or";
|
|
20
|
+
conditions: Cs[number][];
|
|
21
|
+
};
|
|
22
|
+
/** An empty group, to push conditions into later. */
|
|
23
|
+
export declare function and(): {
|
|
24
|
+
type: "and";
|
|
25
|
+
conditions: AnyCondition[];
|
|
26
|
+
};
|
|
27
|
+
/** The group keeps the union of what was passed, so a mixed `or(cond(…), and(…))` keeps each one. */
|
|
28
|
+
export declare function and<Cs extends AnyCondition[]>(...conditions: Cs): {
|
|
29
|
+
type: "and";
|
|
30
|
+
conditions: Cs[number][];
|
|
31
|
+
};
|
|
4
32
|
/**
|
|
5
33
|
* Negate a group: `not(a)` is `NOT a`, and `not(a, b)` is `NOT (a AND b)`.
|
|
6
34
|
*
|
|
@@ -15,8 +43,27 @@ export declare function and(...conditions: (FilterCondition | LogicalCondition)[
|
|
|
15
43
|
* one of them is the query the caller wrote. It also means a negation includes
|
|
16
44
|
* rows whose column is NULL — which is what `NOT` means.
|
|
17
45
|
*/
|
|
18
|
-
export declare function not(
|
|
19
|
-
|
|
46
|
+
export declare function not(): {
|
|
47
|
+
type: "not";
|
|
48
|
+
conditions: AnyCondition[];
|
|
49
|
+
};
|
|
50
|
+
export declare function not<Cs extends AnyCondition[]>(...conditions: Cs): {
|
|
51
|
+
type: "not";
|
|
52
|
+
conditions: Cs[number][];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* One condition: `cond("age", ">=", 18)`.
|
|
56
|
+
*
|
|
57
|
+
* It returns the column, operator and value as written rather than a
|
|
58
|
+
* `FilterCondition`, so that where the group is used on a typed client the
|
|
59
|
+
* column and the value are checked against the row — a typo'd column or a
|
|
60
|
+
* string compared to a number column is a compile error there.
|
|
61
|
+
*/
|
|
62
|
+
export declare function cond<K extends string, Op extends WhereFilterOp, V extends ConditionValue>(column: K, operator: Op, value: V): {
|
|
63
|
+
column: K;
|
|
64
|
+
operator: Op;
|
|
65
|
+
value: V;
|
|
66
|
+
};
|
|
20
67
|
export declare class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {
|
|
21
68
|
private collection;
|
|
22
69
|
private params;
|
|
@@ -86,3 +133,4 @@ export declare class QueryBuilder<M extends Record<string, unknown> = Record<str
|
|
|
86
133
|
*/
|
|
87
134
|
listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
|
|
88
135
|
}
|
|
136
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { normalizeOrderBy } from "./sort-dialect.es.js";
|
|
2
|
+
//#region src/data/query_builder.ts
|
|
3
|
+
function or(...conditions) {
|
|
4
|
+
return {
|
|
5
|
+
type: "or",
|
|
6
|
+
conditions
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function and(...conditions) {
|
|
10
|
+
return {
|
|
11
|
+
type: "and",
|
|
12
|
+
conditions
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function not(...conditions) {
|
|
16
|
+
return {
|
|
17
|
+
type: "not",
|
|
18
|
+
conditions
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One condition: `cond("age", ">=", 18)`.
|
|
23
|
+
*
|
|
24
|
+
* It returns the column, operator and value as written rather than a
|
|
25
|
+
* `FilterCondition`, so that where the group is used on a typed client the
|
|
26
|
+
* column and the value are checked against the row — a typo'd column or a
|
|
27
|
+
* string compared to a number column is a compile error there.
|
|
28
|
+
*/
|
|
29
|
+
function cond(column, operator, value) {
|
|
30
|
+
return {
|
|
31
|
+
column,
|
|
32
|
+
operator,
|
|
33
|
+
value
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var QueryBuilder = class {
|
|
37
|
+
collection;
|
|
38
|
+
params = { where: {} };
|
|
39
|
+
constructor(collection) {
|
|
40
|
+
this.collection = collection;
|
|
41
|
+
}
|
|
42
|
+
where(columnOrCondition, operator, value) {
|
|
43
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
44
|
+
const next = columnOrCondition;
|
|
45
|
+
this.params.logical = this.params.logical ? {
|
|
46
|
+
type: "and",
|
|
47
|
+
conditions: [this.params.logical, next]
|
|
48
|
+
} : next;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
if (!this.params.where) this.params.where = {};
|
|
52
|
+
const column = columnOrCondition;
|
|
53
|
+
const condition = [operator, value];
|
|
54
|
+
const existing = this.params.where[column];
|
|
55
|
+
if (existing === void 0) this.params.where[column] = condition;
|
|
56
|
+
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
57
|
+
else {
|
|
58
|
+
let firstCondition;
|
|
59
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
60
|
+
else firstCondition = ["==", existing];
|
|
61
|
+
this.params.where[column] = [firstCondition, condition];
|
|
62
|
+
}
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Order the results by a specific column.
|
|
67
|
+
*
|
|
68
|
+
* Called again, this adds a tie-breaker rather than replacing the sort:
|
|
69
|
+
* keys apply in the order they were added.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
73
|
+
* @example
|
|
74
|
+
* client.collection('users').orderBy('roles').orderBy('createdAt', 'desc').find()
|
|
75
|
+
*/
|
|
76
|
+
orderBy(column, direction = "asc") {
|
|
77
|
+
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
78
|
+
this.params.orderBy = [...existing, [column, direction]];
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Limit the number of results returned.
|
|
83
|
+
*/
|
|
84
|
+
limit(count) {
|
|
85
|
+
this.params.limit = count;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Skip the first N results.
|
|
90
|
+
*/
|
|
91
|
+
offset(count) {
|
|
92
|
+
this.params.offset = count;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set a free-text search string if supported by the backend.
|
|
97
|
+
*/
|
|
98
|
+
search(searchString, options) {
|
|
99
|
+
this.params.searchString = searchString;
|
|
100
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
105
|
+
*
|
|
106
|
+
* Postgres only, over a property declared as `type: "vector"`. Rows come
|
|
107
|
+
* back with a `_distance`; `where` filters before the ordering.
|
|
108
|
+
*/
|
|
109
|
+
vectorSearch(property, vector, options) {
|
|
110
|
+
this.params.vectorSearch = {
|
|
111
|
+
property,
|
|
112
|
+
vector,
|
|
113
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
114
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
115
|
+
};
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Include related entities in the response.
|
|
120
|
+
* Relations will be populated with full entity data instead of just IDs.
|
|
121
|
+
*
|
|
122
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
123
|
+
* @example
|
|
124
|
+
* // Include specific relations
|
|
125
|
+
* client.data.posts.include("tags", "author").find()
|
|
126
|
+
*
|
|
127
|
+
* // Include all relations
|
|
128
|
+
* client.data.posts.include("*").find()
|
|
129
|
+
*/
|
|
130
|
+
include(...relations) {
|
|
131
|
+
this.params.include = relations;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Execute the find query and return the results.
|
|
136
|
+
*/
|
|
137
|
+
async find() {
|
|
138
|
+
return this.collection.find(this.params);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Listen to realtime updates matching this query.
|
|
142
|
+
*/
|
|
143
|
+
listen(onUpdate, onError) {
|
|
144
|
+
if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
145
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
export { QueryBuilder, and, cond, not, or };
|
|
150
|
+
|
|
151
|
+
//# sourceMappingURL=query_builder.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query_builder.es.js","names":[],"sources":["../../src/data/query_builder.ts"],"sourcesContent":["import {\n CollectionAccessor,\n FilterCondition,\n FindParams,\n FindResponse,\n LogicalCondition,\n OrderByTuple,\n QueryBuilderInterface,\n WhereFilterOp,\n WhereValueFor,\n type ComputedSortField\n} from \"@rebasepro/types\";\nimport { normalizeOrderBy } from \"./sort-dialect\";\n\n/**\n * Any condition a group may hold. The helpers below are generic over it so the\n * group they return keeps each condition's column, operator and value exactly\n * as written — which is what lets a typed `logical` / `.where(group)` check\n * them against the row ({@link LogicalConditionFor}). Their results are still\n * plain `LogicalCondition`s anywhere one is expected.\n */\ntype AnyCondition = FilterCondition | LogicalCondition;\n\n/** A value as `cond` keeps it: literals stay literals, so `\"draft\"` can match a `\"draft\" | \"published\"` column. */\ntype ConditionValue = string | number | bigint | boolean | symbol | object | null | undefined;\n\n/** An empty group, to push conditions into later. */\nexport function or(): { type: \"or\"; conditions: AnyCondition[] };\n/** The group keeps the union of what was passed, so a mixed `or(cond(…), and(…))` keeps each one. */\nexport function or<Cs extends AnyCondition[]>(...conditions: Cs): { type: \"or\"; conditions: Cs[number][] };\nexport function or(...conditions: AnyCondition[]): LogicalCondition {\n return { type: \"or\",\nconditions };\n}\n\n/** An empty group, to push conditions into later. */\nexport function and(): { type: \"and\"; conditions: AnyCondition[] };\n/** The group keeps the union of what was passed, so a mixed `or(cond(…), and(…))` keeps each one. */\nexport function and<Cs extends AnyCondition[]>(...conditions: Cs): { type: \"and\"; conditions: Cs[number][] };\nexport function and(...conditions: AnyCondition[]): LogicalCondition {\n return { type: \"and\",\nconditions };\n}\n\n/**\n * Negate a group: `not(a)` is `NOT a`, and `not(a, b)` is `NOT (a AND b)`.\n *\n * The conjunction, not the disjunction — one rule, stated on\n * {@link LogicalCondition} and applied identically by the wire codec, the REST\n * `?not=` parameter and every driver compiler. Groups nest, so De Morgan's\n * other half is `not(or(a, b))`.\n *\n * It compiles to a real SQL `NOT (...)` rather than to inverted operators,\n * which matters more than it looks: SQL is three-valued, so `NOT (a AND b)` and\n * `(NOT a) OR (NOT b)` stop agreeing the moment a NULL is involved, and only\n * one of them is the query the caller wrote. It also means a negation includes\n * rows whose column is NULL — which is what `NOT` means.\n */\nexport function not(): { type: \"not\"; conditions: AnyCondition[] };\nexport function not<Cs extends AnyCondition[]>(...conditions: Cs): { type: \"not\"; conditions: Cs[number][] };\nexport function not(...conditions: AnyCondition[]): LogicalCondition {\n return { type: \"not\",\nconditions };\n}\n\n/**\n * One condition: `cond(\"age\", \">=\", 18)`.\n *\n * It returns the column, operator and value as written rather than a\n * `FilterCondition`, so that where the group is used on a typed client the\n * column and the value are checked against the row — a typo'd column or a\n * string compared to a number column is a compile error there.\n */\nexport function cond<K extends string, Op extends WhereFilterOp, V extends ConditionValue>(\n column: K,\n operator: Op,\n value: V\n): { column: K; operator: Op; value: V } {\n return { column,\noperator,\nvalue };\n}\n\nexport class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {\n // Keyed by plain `string` on purpose: it is written in place by the\n // methods below, whose own parameters are typed against `M`, and a\n // `Partial<Record<FieldPath<M>, …>>` is read-only under a generic `M`\n // (TS2862). The typing users see is on the methods; this is the buffer\n // behind them, cast once at each handoff.\n private params: FindParams = { where: {} };\n\n constructor(private collection: CollectionAccessor<M>) {}\n\n /**\n * Add a filter condition to your query.\n * @example\n * client.collection('users').where('age', '>=', 18).find()\n */\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;\n where(logicalCondition: LogicalCondition): this;\n where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown): this {\n // Handle LogicalCondition signature\n if (typeof columnOrCondition === \"object\" && columnOrCondition !== null && \"type\" in columnOrCondition) {\n // A second group narrows rather than replaces — see the SDK builder\n // in `@rebasepro/client`, which had the same defect: every other\n // `.where()` adds a condition, so the one that silently dropped the\n // previous group was also the one that widened the result set.\n const next = columnOrCondition as LogicalCondition;\n this.params.logical = this.params.logical\n ? { type: \"and\", conditions: [this.params.logical, next] }\n : next;\n return this;\n }\n\n if (!this.params.where) {\n this.params.where = {};\n }\n\n const column = columnOrCondition as string;\n const condition: [WhereFilterOp, unknown] = [operator!, value];\n const existing = this.params.where[column];\n\n if (existing === undefined) {\n this.params.where[column] = condition;\n } else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {\n (this.params.where[column] as [WhereFilterOp, unknown][]).push(condition);\n } else {\n // Convert existing single tuple/value into array of tuples\n let firstCondition: [WhereFilterOp, unknown];\n if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === \"string\") {\n firstCondition = existing as [WhereFilterOp, unknown];\n } else {\n firstCondition = [\"==\", existing];\n }\n this.params.where[column] = [firstCondition, condition];\n }\n\n return this;\n }\n\n /**\n * Order the results by a specific column.\n *\n * Called again, this adds a tie-breaker rather than replacing the sort:\n * keys apply in the order they were added.\n *\n * @example\n * client.collection('users').orderBy('createdAt', 'desc').find()\n * @example\n * client.collection('users').orderBy('roles').orderBy('createdAt', 'desc').find()\n */\n orderBy(column: (keyof M & string) | ComputedSortField, direction: \"asc\" | \"desc\" = \"asc\"): this {\n const existing = normalizeOrderBy(this.params.orderBy) ?? [];\n this.params.orderBy = [...existing, [column, direction] as OrderByTuple];\n return this;\n }\n\n /**\n * Limit the number of results returned.\n */\n limit(count: number): this {\n this.params.limit = count;\n return this;\n }\n\n /**\n * Skip the first N results.\n */\n offset(count: number): this {\n this.params.offset = count;\n return this;\n }\n\n /**\n * Set a free-text search string if supported by the backend.\n */\n search(searchString: string, options?: { explain?: boolean }): this {\n this.params.searchString = searchString;\n if (options?.explain !== undefined) this.params.searchExplain = options.explain;\n return this;\n }\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Rows come\n * back with a `_distance`; `where` filters before the ordering.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): this {\n this.params.vectorSearch = {\n property,\n vector,\n ...(options?.distance !== undefined && { distance: options.distance }),\n ...(options?.threshold !== undefined && { threshold: options.threshold })\n };\n return this;\n }\n\n /**\n * Include related entities in the response.\n * Relations will be populated with full entity data instead of just IDs.\n *\n * @param relations - Relation names to include, or \"*\" for all.\n * @example\n * // Include specific relations\n * client.data.posts.include(\"tags\", \"author\").find()\n *\n * // Include all relations\n * client.data.posts.include(\"*\").find()\n */\n include(...relations: string[]): this {\n this.params.include = relations;\n return this;\n }\n\n /**\n * Execute the find query and return the results.\n */\n async find(): Promise<FindResponse<M>> {\n return this.collection.find(this.params as FindParams<M>) as Promise<FindResponse<M>>;\n }\n\n /**\n * Listen to realtime updates matching this query.\n */\n listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void {\n if (!this.collection.listen) {\n throw new Error(\"Listen is only available when RebaseClient is configured with a websocketUrl.\");\n }\n return this.collection.listen(this.params as FindParams<M>, onUpdate, onError);\n }\n}\n"],"mappings":";;AA8BA,SAAgB,GAAG,GAAG,YAA8C;CAChE,OAAO;EAAE,MAAM;EACnB;CAAW;AACX;AAMA,SAAgB,IAAI,GAAG,YAA8C;CACjE,OAAO;EAAE,MAAM;EACnB;CAAW;AACX;AAkBA,SAAgB,IAAI,GAAG,YAA8C;CACjE,OAAO;EAAE,MAAM;EACnB;CAAW;AACX;;;;;;;;;AAUA,SAAgB,KACZ,QACA,UACA,OACqC;CACrC,OAAO;EAAE;EACb;EACA;CAAM;AACN;AAEA,IAAa,eAAb,MAA2H;CAQnG;CAFpB,SAA6B,EAAE,OAAO,CAAC,EAAE;CAEzC,YAAY,YAA2C;EAAnC,KAAA,aAAA;CAAoC;CASxD,MAAM,mBAA8C,UAA0B,OAAuB;EAEjG,IAAI,OAAO,sBAAsB,YAAY,sBAAsB,QAAQ,UAAU,mBAAmB;GAKpG,MAAM,OAAO;GACb,KAAK,OAAO,UAAU,KAAK,OAAO,UAC5B;IAAE,MAAM;IAAO,YAAY,CAAC,KAAK,OAAO,SAAS,IAAI;GAAE,IACvD;GACN,OAAO;EACX;EAEA,IAAI,CAAC,KAAK,OAAO,OACb,KAAK,OAAO,QAAQ,CAAC;EAGzB,MAAM,SAAS;EACf,MAAM,YAAsC,CAAC,UAAW,KAAK;EAC7D,MAAM,WAAW,KAAK,OAAO,MAAM;EAEnC,IAAI,aAAa,KAAA,GACb,KAAK,OAAO,MAAM,UAAU;OACzB,IAAI,MAAM,QAAQ,QAAQ,KAAK,SAAS,SAAS,KAAK,MAAM,QAAQ,SAAS,EAAE,GAClF,KAAM,OAAO,MAAM,OAAO,CAAgC,KAAK,SAAS;OACrE;GAEH,IAAI;GACJ,IAAI,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,OAAO,UAC3E,iBAAiB;QAEjB,iBAAiB,CAAC,MAAM,QAAQ;GAEpC,KAAK,OAAO,MAAM,UAAU,CAAC,gBAAgB,SAAS;EAC1D;EAEA,OAAO;CACX;;;;;;;;;;;;CAaA,QAAQ,QAAgD,YAA4B,OAAa;EAC7F,MAAM,WAAW,iBAAiB,KAAK,OAAO,OAAO,KAAK,CAAC;EAC3D,KAAK,OAAO,UAAU,CAAC,GAAG,UAAU,CAAC,QAAQ,SAAS,CAAiB;EACvE,OAAO;CACX;;;;CAKA,MAAM,OAAqB;EACvB,KAAK,OAAO,QAAQ;EACpB,OAAO;CACX;;;;CAKA,OAAO,OAAqB;EACxB,KAAK,OAAO,SAAS;EACrB,OAAO;CACX;;;;CAKA,OAAO,cAAsB,SAAuC;EAChE,KAAK,OAAO,eAAe;EAC3B,IAAI,SAAS,YAAY,KAAA,GAAW,KAAK,OAAO,gBAAgB,QAAQ;EACxE,OAAO;CACX;;;;;;;CAQA,aACI,UACA,QACA,SACI;EACJ,KAAK,OAAO,eAAe;GACvB;GACA;GACA,GAAI,SAAS,aAAa,KAAA,KAAa,EAAE,UAAU,QAAQ,SAAS;GACpE,GAAI,SAAS,cAAc,KAAA,KAAa,EAAE,WAAW,QAAQ,UAAU;EAC3E;EACA,OAAO;CACX;;;;;;;;;;;;;CAcA,QAAQ,GAAG,WAA2B;EAClC,KAAK,OAAO,UAAU;EACtB,OAAO;CACX;;;;CAKA,MAAM,OAAiC;EACnC,OAAO,KAAK,WAAW,KAAK,KAAK,MAAuB;CAC5D;;;;CAKA,OAAO,UAA2C,SAA8C;EAC5F,IAAI,CAAC,KAAK,WAAW,QACjB,MAAM,IAAI,MAAM,+EAA+E;EAEnG,OAAO,KAAK,WAAW,OAAO,KAAK,QAAyB,UAAU,OAAO;CACjF;AACJ"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DEFAULT_DATA_SOURCE_KEY, getDataSourceCapabilities } from "@rebasepro/types";
|
|
2
|
+
//#region src/data/resolveDataSource.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
5
|
+
* Later entries win on key collision.
|
|
6
|
+
*/
|
|
7
|
+
function createDataSourceRegistry(definitions) {
|
|
8
|
+
const registry = {};
|
|
9
|
+
for (const def of definitions ?? []) registry[def.key] = def;
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the effective data source for a collection — the single source of
|
|
14
|
+
* truth shared by the frontend router, the backend driver registry, and the
|
|
15
|
+
* editor's capability lookups.
|
|
16
|
+
*
|
|
17
|
+
* Resolution order:
|
|
18
|
+
* 1. The routing **key** is `collection.dataSource`, else
|
|
19
|
+
* {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
20
|
+
* 2. If a definition is registered for that key, it provides `engine`,
|
|
21
|
+
* `transport`, and `databaseId`.
|
|
22
|
+
* 3. Otherwise values are synthesized: `engine` from `collection.engine`
|
|
23
|
+
* (or the key, or `"postgres"`), `transport` defaults to `"server"`,
|
|
24
|
+
* and `databaseId` from the collection.
|
|
25
|
+
*
|
|
26
|
+
* `capabilities` are always derived from the resolved `engine`, so two
|
|
27
|
+
* data sources sharing an engine share capabilities.
|
|
28
|
+
*
|
|
29
|
+
* @param collection the collection (or any object carrying the routing fields)
|
|
30
|
+
* @param registry optional registry of declared data sources
|
|
31
|
+
*/
|
|
32
|
+
function resolveDataSource(collection, registry) {
|
|
33
|
+
const key = collection?.dataSource ?? DEFAULT_DATA_SOURCE_KEY;
|
|
34
|
+
const def = registry?.[key];
|
|
35
|
+
const engine = def?.engine ?? collection?.engine ?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
36
|
+
return {
|
|
37
|
+
key,
|
|
38
|
+
engine,
|
|
39
|
+
transport: def?.transport ?? "server",
|
|
40
|
+
databaseId: collection?.databaseId ?? def?.databaseId,
|
|
41
|
+
capabilities: getDataSourceCapabilities(engine)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Does a SQL toolchain own this collection's storage?
|
|
46
|
+
*
|
|
47
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
48
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
49
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
50
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
51
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
52
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
53
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
54
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
55
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
56
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
57
|
+
*
|
|
58
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
59
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
60
|
+
* gets the same treatment as the built-in ones.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
63
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
64
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
65
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
66
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
67
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
68
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
69
|
+
*/
|
|
70
|
+
function isRelationalCollection(collection, registry) {
|
|
71
|
+
return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
75
|
+
* {@link isRelationalCollection}.
|
|
76
|
+
*
|
|
77
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
78
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
79
|
+
* keeps the input order.
|
|
80
|
+
*/
|
|
81
|
+
function relationalCollections(collections, registry) {
|
|
82
|
+
return collections.filter((collection) => isRelationalCollection(collection, registry));
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { createDataSourceRegistry, isRelationalCollection, relationalCollections, resolveDataSource };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=resolveDataSource.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveDataSource.es.js","names":[],"sources":["../../src/data/resolveDataSource.ts"],"sourcesContent":["import {\n DataSourceDefinition,\n ResolvedDataSource,\n DEFAULT_DATA_SOURCE_KEY,\n getDataSourceCapabilities\n} from \"@rebasepro/types\";\n\n/**\n * The subset of a collection needed to resolve its data source. Accepting a\n * structural type (rather than the full `CollectionConfig`) keeps this usable\n * from anywhere — frontend router, backend registry, editor — without coupling\n * to the collection union.\n */\nexport interface DataSourceResolvable {\n /** Preferred routing key. */\n dataSource?: string;\n /** Engine type discriminant (set on variant collection types). */\n engine?: string;\n /** Within-engine instance. */\n databaseId?: string;\n}\n\n/** A lookup of data-source definitions by key. */\nexport type DataSourceRegistry = Record<string, DataSourceDefinition>;\n\n/**\n * Build a keyed registry from a list of {@link DataSourceDefinition}s.\n * Later entries win on key collision.\n */\nexport function createDataSourceRegistry(definitions?: DataSourceDefinition[]): DataSourceRegistry {\n const registry: DataSourceRegistry = {};\n for (const def of definitions ?? []) {\n registry[def.key] = def;\n }\n return registry;\n}\n\n/**\n * Resolve the effective data source for a collection — the single source of\n * truth shared by the frontend router, the backend driver registry, and the\n * editor's capability lookups.\n *\n * Resolution order:\n * 1. The routing **key** is `collection.dataSource`, else\n * {@link DEFAULT_DATA_SOURCE_KEY}.\n * 2. If a definition is registered for that key, it provides `engine`,\n * `transport`, and `databaseId`.\n * 3. Otherwise values are synthesized: `engine` from `collection.engine`\n * (or the key, or `\"postgres\"`), `transport` defaults to `\"server\"`,\n * and `databaseId` from the collection.\n *\n * `capabilities` are always derived from the resolved `engine`, so two\n * data sources sharing an engine share capabilities.\n *\n * @param collection the collection (or any object carrying the routing fields)\n * @param registry optional registry of declared data sources\n */\nexport function resolveDataSource(\n collection: DataSourceResolvable | undefined,\n registry?: DataSourceRegistry\n): ResolvedDataSource {\n const key = collection?.dataSource ?? DEFAULT_DATA_SOURCE_KEY;\n const def = registry?.[key];\n\n const engine = def?.engine\n ?? collection?.engine\n ?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : \"postgres\");\n\n const transport = def?.transport ?? \"server\";\n const databaseId = collection?.databaseId ?? def?.databaseId;\n\n return {\n key,\n engine,\n transport,\n databaseId,\n capabilities: getDataSourceCapabilities(engine)\n };\n}\n\n/**\n * Does a SQL toolchain own this collection's storage?\n *\n * \"Owns the storage\" means: something generates a table for it, pushes that\n * table to a database, plans its RLS policies, and reports it as drifted when\n * the two disagree. That is true of a Postgres collection and false of a\n * Firestore or MongoDB one, whose documents live in a store Rebase never\n * migrates — and the two were never told apart. Every stage of the SQL\n * toolchain took \"the collections\" to mean *all* of them, so a Firestore\n * collection declared next to the Postgres ones got a `pgTable` in the\n * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the\n * `db push` include list — where its name shielding a same-named real table\n * from Atlas's exclude list is the one that can lose data.\n *\n * The answer is the resolved engine's {@link DataSourceCapabilities}, not a\n * name check: an engine registered through `registerDataSourceCapabilities`\n * gets the same treatment as the built-in ones.\n *\n * Deliberately answers **true** for an engine nobody has heard of. Build-time\n * tooling (the CLI, the schema generator) has no data-source registry to\n * resolve a `dataSource` key against, so an unknown key resolves to an unknown\n * engine — and the cost of the two mistakes is not symmetric. Wrongly\n * including a collection generates a table nothing writes to; wrongly excluding\n * one silently stops generating a table the app is serving from. Declare\n * `engine` on a collection that is not SQL-backed and this is exact.\n */\nexport function isRelationalCollection(\n collection: DataSourceResolvable | undefined,\n registry?: DataSourceRegistry\n): boolean {\n // The collection's own `engine` wins over a registered definition's. That\n // is the opposite of {@link resolveDataSource}'s precedence, deliberately:\n // there a definition describes where the data *goes*, so it should override;\n // here the question is what the author said this collection is, and a\n // collection declaring `engine: \"firestore\"` with no `dataSource` must not\n // come back as the default source's engine and be handed a table.\n const engine = collection?.engine\n ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : undefined);\n return getDataSourceCapabilities(engine).supportsRelations;\n}\n\n/**\n * The subset of `collections` a SQL toolchain owns — see\n * {@link isRelationalCollection}.\n *\n * Every stage that generates SQL from collections starts by calling this, so\n * the rule lives in one place rather than being re-decided per generator. It\n * keeps the input order.\n */\nexport function relationalCollections<C extends DataSourceResolvable>(\n collections: readonly C[],\n registry?: DataSourceRegistry\n): C[] {\n return collections.filter(collection => isRelationalCollection(collection, registry));\n}\n"],"mappings":";;;;;;AA6BA,SAAgB,yBAAyB,aAA0D;CAC/F,MAAM,WAA+B,CAAC;CACtC,KAAK,MAAM,OAAO,eAAe,CAAC,GAC9B,SAAS,IAAI,OAAO;CAExB,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,kBACZ,YACA,UACkB;CAClB,MAAM,MAAM,YAAY,cAAc;CACtC,MAAM,MAAM,WAAW;CAEvB,MAAM,SAAS,KAAK,UACb,YAAY,WACX,QAAQ,0BAA0B,MAAM;CAKhD,OAAO;EACH;EACA;EACA,WANc,KAAK,aAAa;EAOhC,YANe,YAAY,cAAc,KAAK;EAO9C,cAAc,0BAA0B,MAAM;CAClD;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,uBACZ,YACA,UACO;CASP,OAAO,0BAFQ,YAAY,WACnB,YAAY,aAAa,kBAAkB,YAAY,QAAQ,CAAC,CAAC,SAAS,KAAA,EAC3C,CAAC,CAAC;AAC7C;;;;;;;;;AAUA,SAAgB,sBACZ,aACA,UACG;CACH,OAAO,YAAY,QAAO,eAAc,uBAAuB,YAAY,QAAQ,CAAC;AACxF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A row's version — the `ETag` the read that produced it reported — kept on
|
|
3
|
+
* the row without being part of it.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Where a fetched row's version is kept.
|
|
9
|
+
*
|
|
10
|
+
* Non-enumerable, so it does not appear in `Object.keys`, in `JSON.stringify`,
|
|
11
|
+
* in a spread into an update body, or in the generated `Row` type — a row's
|
|
12
|
+
* version is a fact about the *response*, not a column, and putting it in the
|
|
13
|
+
* row proper would mean the server having to strip it back off every write.
|
|
14
|
+
*
|
|
15
|
+
* A string key rather than a symbol so it survives the structured clone an
|
|
16
|
+
* offline store or a worker boundary does… which it does not, and that is the
|
|
17
|
+
* honest position: an etag is per-read, and a row that has been through a cache
|
|
18
|
+
* has not been read from the server. `etagOf` answers `undefined` there, and a
|
|
19
|
+
* conditional write without a tag is an unconditional one.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ETAG_KEY = "__etag";
|
|
22
|
+
/**
|
|
23
|
+
* The version of a row, when it came straight from a read that reported one.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const post = await client.data.posts.get(1);
|
|
27
|
+
* await client.data.posts.update(1, { title: "New" }, { ifMatch: etagOf(post) });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Set on a row from `findById()`/`get()`, from `update()`, and on every row
|
|
31
|
+
* `listenById()` delivers (the SDK asks the server to tag each frame).
|
|
32
|
+
* `undefined` for a row from `find()` (a list response carries one ETag at
|
|
33
|
+
* most, and it would not be this row's), from the offline cache, or from a
|
|
34
|
+
* server that does not send them. Passing `undefined` as `ifMatch` sends no
|
|
35
|
+
* precondition, so the call above degrades to an ordinary update rather than
|
|
36
|
+
* failing — which is the right default, and the reason to read this at the call
|
|
37
|
+
* site where the choice is visible.
|
|
38
|
+
*/
|
|
39
|
+
export declare function etagOf(row: unknown): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Attach a version to a row without making it part of the row: a
|
|
42
|
+
* non-enumerable `__etag`, so it stays out of `Object.keys`, a spread and
|
|
43
|
+
* `JSON.stringify`, and a row copied through any of them has no tag — which
|
|
44
|
+
* is right, since the copy was not read from the server.
|
|
45
|
+
*/
|
|
46
|
+
export declare function withETag<T>(row: T, etag: string | undefined): T;
|