@rebasepro/common 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/collections/CollectionRegistry.d.ts +1 -1
- package/dist/collections/default-collections.d.ts +15 -84
- package/dist/data/buildRebaseData.d.ts +1 -1
- package/dist/data/filter-dialect.d.ts +11 -0
- package/dist/data/sort-dialect.d.ts +15 -3
- package/dist/index.es.js +375 -63
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +69 -24
- package/dist/util/callback-errors.d.ts +77 -0
- package/dist/util/callback-errors.test.d.ts +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/policy/evaluatePolicy.d.ts +6 -0
- package/dist/util/relations.d.ts +41 -0
- package/dist/util/table-name.test.d.ts +1 -0
- package/package.json +26 -22
- package/src/collections/CollectionRegistry.ts +0 -485
- package/src/collections/default-collections.ts +0 -109
- package/src/collections/index.ts +0 -2
- package/src/data/buildRebaseData.ts +0 -816
- package/src/data/buildRoutedRebaseData.ts +0 -103
- package/src/data/filter-conditions.ts +0 -46
- package/src/data/filter-dialect.ts +0 -737
- package/src/data/paginate.ts +0 -334
- package/src/data/query_builder.ts +0 -176
- package/src/data/resolveDataSource.ts +0 -135
- package/src/data/sort-dialect.ts +0 -237
- package/src/index.ts +0 -11
- package/src/table-classification.ts +0 -109
- package/src/types/json-logic-js.d.ts +0 -8
- package/src/util/auth-default-policies.ts +0 -215
- package/src/util/builders.ts +0 -82
- package/src/util/callbacks.ts +0 -122
- package/src/util/collections.ts +0 -117
- package/src/util/common.ts +0 -2
- package/src/util/conditions.ts +0 -168
- package/src/util/email.ts +0 -32
- package/src/util/entities.ts +0 -282
- package/src/util/enums.ts +0 -26
- package/src/util/identity.ts +0 -202
- package/src/util/index.ts +0 -21
- package/src/util/internal-tables.test.ts +0 -188
- package/src/util/internal-tables.ts +0 -197
- package/src/util/junction-policies.ts +0 -355
- package/src/util/paths.ts +0 -27
- package/src/util/permissions.test.ts +0 -866
- package/src/util/permissions.ts +0 -206
- package/src/util/pg-column-to-property.ts +0 -377
- package/src/util/policy/evaluatePolicy.ts +0 -194
- package/src/util/policy/index.ts +0 -4
- package/src/util/policy/policyToPostgres.ts +0 -263
- package/src/util/policy/securityRuleToConditions.ts +0 -67
- package/src/util/policy/sqlToPolicy.ts +0 -422
- package/src/util/relations.ts +0 -236
- package/src/util/resolutions.ts +0 -534
- package/src/util/resolve-relation.ts +0 -243
- package/src/util/storage.ts +0 -177
- package/src/util/string-column-length.ts +0 -31
package/src/data/paginate.ts
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_LIST_LIMIT,
|
|
3
|
-
FilterValues,
|
|
4
|
-
FieldPath,
|
|
5
|
-
FindAllParams,
|
|
6
|
-
FindParams,
|
|
7
|
-
FindResult,
|
|
8
|
-
IterateParams,
|
|
9
|
-
WhereFilterOp
|
|
10
|
-
} from "@rebasepro/types";
|
|
11
|
-
import { normalizeOrderBy } from "./sort-dialect";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The pagination engine behind `iterate()` / `findAll()`.
|
|
15
|
-
*
|
|
16
|
-
* It lives here, above both transports, on purpose: the HTTP client and the
|
|
17
|
-
* in-process accessor implement the same `SDKCollectionClient` contract, and a
|
|
18
|
-
* helper written twice is a helper that drifts. Both call into this file, so
|
|
19
|
-
* "the SDK paginates like *this*" has exactly one definition.
|
|
20
|
-
*
|
|
21
|
-
* Everything below is expressed in terms of a single `find(params)` function,
|
|
22
|
-
* which is all either transport has to supply.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/** Rows requested per page when the caller does not say. */
|
|
26
|
-
export const DEFAULT_PAGE_SIZE = 200;
|
|
27
|
-
|
|
28
|
-
/** Rows `findAll()` will materialise before it refuses to continue. */
|
|
29
|
-
export const DEFAULT_FIND_ALL_MAX_ROWS = 10_000;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Requests one walk may make before it gives up on the server ever saying
|
|
33
|
-
* `hasMore: false`. At the default page size that is two million rows — far
|
|
34
|
-
* past any legitimate walk, and short of running forever.
|
|
35
|
-
*/
|
|
36
|
-
export const DEFAULT_MAX_PAGES = 10_000;
|
|
37
|
-
|
|
38
|
-
/** Why a pagination walk refused to continue. */
|
|
39
|
-
export type PaginationErrorCode =
|
|
40
|
-
/** `findAll()` matched more rows than its ceiling allows. */
|
|
41
|
-
| "max-rows"
|
|
42
|
-
/** The walk made its maximum number of requests without the server finishing. */
|
|
43
|
-
| "max-pages"
|
|
44
|
-
/** A cursor row carried no value for the cursor column. */
|
|
45
|
-
| "cursor-missing"
|
|
46
|
-
/** Two consecutive pages ended on the same cursor value, so the walk cannot advance. */
|
|
47
|
-
| "cursor-stalled"
|
|
48
|
-
/** A `cursor` was asked for on one column while `orderBy` sorted by another. */
|
|
49
|
-
| "cursor-order-mismatch";
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Thrown when a walk stops for a reason the caller needs to know about.
|
|
53
|
-
*
|
|
54
|
-
* Every one of these is a case where the alternative would be silent: a
|
|
55
|
-
* truncated array that looks complete, or a loop that never returns. Check
|
|
56
|
-
* {@link code} to tell them apart.
|
|
57
|
-
*/
|
|
58
|
-
export class RebasePaginationError extends Error {
|
|
59
|
-
readonly code: PaginationErrorCode;
|
|
60
|
-
|
|
61
|
-
constructor(code: PaginationErrorCode, message: string) {
|
|
62
|
-
super(message);
|
|
63
|
-
this.name = "RebasePaginationError";
|
|
64
|
-
this.code = code;
|
|
65
|
-
// Keeps `instanceof` working when this is compiled down for an older
|
|
66
|
-
// target, where extending a builtin otherwise loses the prototype.
|
|
67
|
-
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** The one thing a transport has to provide to be paginated. */
|
|
72
|
-
export type PageFinder<M extends Record<string, unknown> = Record<string, unknown>> =
|
|
73
|
-
(params: FindParams<M>) => Promise<FindResult<M>>;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
77
|
-
*
|
|
78
|
-
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
79
|
-
* transport has to mean the same thing by "page two". Four of them did not —
|
|
80
|
-
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
81
|
-
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
82
|
-
* the published type documented a fourth number. Pages that overlap or skip
|
|
83
|
-
* rows are the mildest of those outcomes.
|
|
84
|
-
*
|
|
85
|
-
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
86
|
-
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
87
|
-
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
88
|
-
* look like it is paging by offset.
|
|
89
|
-
*/
|
|
90
|
-
export function resolveFindWindow(
|
|
91
|
-
params?: Pick<FindParams, "limit" | "offset" | "page">
|
|
92
|
-
): { limit: number; offset: number; driverOffset: number | undefined } {
|
|
93
|
-
const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
|
|
94
|
-
const offset = params?.page != null
|
|
95
|
-
? Math.max(0, (params.page - 1) * limit)
|
|
96
|
-
: (params?.offset ?? 0);
|
|
97
|
-
return {
|
|
98
|
-
limit,
|
|
99
|
-
offset,
|
|
100
|
-
driverOffset: params?.page != null ? offset : params?.offset
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function normalizePageSize(raw: number | undefined): number {
|
|
105
|
-
if (raw === undefined || !Number.isFinite(raw)) return DEFAULT_PAGE_SIZE;
|
|
106
|
-
return Math.max(1, Math.floor(raw));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function normalizeMaxPages(raw: number | undefined): number {
|
|
110
|
-
if (raw === undefined) return DEFAULT_MAX_PAGES;
|
|
111
|
-
if (raw === Number.POSITIVE_INFINITY) return raw;
|
|
112
|
-
if (!Number.isFinite(raw)) return DEFAULT_MAX_PAGES;
|
|
113
|
-
return Math.max(1, Math.floor(raw));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizeMaxRows(raw: number | undefined): number {
|
|
117
|
-
if (raw === undefined) return DEFAULT_FIND_ALL_MAX_ROWS;
|
|
118
|
-
if (raw === Number.POSITIVE_INFINITY) return raw;
|
|
119
|
-
if (!Number.isFinite(raw)) return DEFAULT_FIND_ALL_MAX_ROWS;
|
|
120
|
-
return Math.max(0, Math.floor(raw));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Add one condition to a `where` map without disturbing what is already there.
|
|
125
|
-
*
|
|
126
|
-
* The caller's own filter on the cursor column has to survive — dropping it
|
|
127
|
-
* would widen the query, which is the silent-filter-loss failure mode — so a
|
|
128
|
-
* second condition on the same column becomes the array-of-tuples form that
|
|
129
|
-
* `FindParams.where` already accepts, and both are AND-ed.
|
|
130
|
-
*/
|
|
131
|
-
function appendCondition<M extends Record<string, unknown>>(
|
|
132
|
-
where: FilterValues<FieldPath<M>> | undefined,
|
|
133
|
-
column: string,
|
|
134
|
-
condition: [WhereFilterOp, unknown]
|
|
135
|
-
): FilterValues<FieldPath<M>> {
|
|
136
|
-
const next = { ...(where ?? {}) } as Record<string, unknown>;
|
|
137
|
-
const existing = next[column];
|
|
138
|
-
if (existing === undefined) {
|
|
139
|
-
next[column] = condition;
|
|
140
|
-
} else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {
|
|
141
|
-
next[column] = [...(existing as [WhereFilterOp, unknown][]), condition];
|
|
142
|
-
} else {
|
|
143
|
-
next[column] = [existing, condition];
|
|
144
|
-
}
|
|
145
|
-
return next as FilterValues<FieldPath<M>>;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function cursorEquals(a: unknown, b: unknown): boolean {
|
|
149
|
-
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
150
|
-
return Object.is(a, b);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Walk every row a query matches, yielding one row at a time and fetching the
|
|
155
|
-
* next page only when the consumer asks for it.
|
|
156
|
-
*
|
|
157
|
-
* See {@link SDKCollectionClient.iterate} for the caller-facing contract,
|
|
158
|
-
* including the offset-drift caveat and the `cursor` alternative.
|
|
159
|
-
*
|
|
160
|
-
* @param find the transport's single-page read
|
|
161
|
-
* @param params `find()` parameters minus the window, plus the walk options
|
|
162
|
-
* @param label the collection name, so an error says which walk failed
|
|
163
|
-
*/
|
|
164
|
-
export async function* paginateFind<M extends Record<string, unknown> = Record<string, unknown>>(
|
|
165
|
-
find: PageFinder<M>,
|
|
166
|
-
params?: IterateParams<M>,
|
|
167
|
-
label = "collection"
|
|
168
|
-
): AsyncGenerator<M, void, undefined> {
|
|
169
|
-
const {
|
|
170
|
-
pageSize,
|
|
171
|
-
cursor,
|
|
172
|
-
maxPages,
|
|
173
|
-
...rest
|
|
174
|
-
} = (params ?? {}) as IterateParams<M> & Record<string, unknown>;
|
|
175
|
-
|
|
176
|
-
const findParams = { ...rest } as FindParams<M>;
|
|
177
|
-
const size = normalizePageSize(pageSize as number | undefined);
|
|
178
|
-
const pageCap = normalizeMaxPages(maxPages as number | undefined);
|
|
179
|
-
|
|
180
|
-
// ── Cursor (keyset) setup ────────────────────────────────────────────────
|
|
181
|
-
const cursorField = typeof cursor === "string" ? cursor : cursor?.field;
|
|
182
|
-
const requestedDirection = (typeof cursor === "object" && cursor !== null)
|
|
183
|
-
? cursor.direction
|
|
184
|
-
: undefined;
|
|
185
|
-
|
|
186
|
-
let direction: "asc" | "desc" = "asc";
|
|
187
|
-
if (cursorField) {
|
|
188
|
-
const orderBy = normalizeOrderBy(findParams.orderBy);
|
|
189
|
-
// A seek is one `>`/`<` on one column, so it can only follow a sort of
|
|
190
|
-
// one column. Over a multi-key sort the same comparison both repeats
|
|
191
|
-
// rows (every later key's ties) and skips them, which is the failure
|
|
192
|
-
// this error exists to prevent — name it rather than seek anyway.
|
|
193
|
-
if (orderBy && orderBy.length > 1) {
|
|
194
|
-
throw new RebasePaginationError(
|
|
195
|
-
"cursor-order-mismatch",
|
|
196
|
-
`Cannot seek on "${cursorField}" while ordering "${label}" by ` +
|
|
197
|
-
`${orderBy.map(([field]) => `"${field}"`).join(", ")}: ` +
|
|
198
|
-
`keyset pagination advances along a single column. ` +
|
|
199
|
-
`Order by "${cursorField}" alone, or drop the cursor and page by offset.`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
if (orderBy && orderBy[0][0] !== cursorField) {
|
|
203
|
-
throw new RebasePaginationError(
|
|
204
|
-
"cursor-order-mismatch",
|
|
205
|
-
`Cannot seek on "${cursorField}" while ordering "${label}" by "${orderBy[0][0]}": ` +
|
|
206
|
-
`keyset pagination only advances along the column the query is sorted by. ` +
|
|
207
|
-
`Order by "${cursorField}", or drop the cursor and page by offset.`
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
direction = requestedDirection ?? orderBy?.[0][1] ?? "asc";
|
|
211
|
-
findParams.orderBy = [cursorField, direction] as FindParams<M>["orderBy"];
|
|
212
|
-
}
|
|
213
|
-
const seekOp: WhereFilterOp = direction === "desc" ? "<" : ">";
|
|
214
|
-
const baseWhere = findParams.where;
|
|
215
|
-
|
|
216
|
-
let offset = 0;
|
|
217
|
-
let pages = 0;
|
|
218
|
-
let cursorValue: unknown;
|
|
219
|
-
let seeking = false;
|
|
220
|
-
|
|
221
|
-
for (;;) {
|
|
222
|
-
if (pages >= pageCap) {
|
|
223
|
-
throw new RebasePaginationError(
|
|
224
|
-
"max-pages",
|
|
225
|
-
`Iterating "${label}" made ${pages} requests without the server reporting the end of ` +
|
|
226
|
-
`the collection. Stopping rather than looping forever — raise \`maxPages\` if the walk ` +
|
|
227
|
-
`is genuinely this long, or check that the backend sets \`meta.hasMore\`.`
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const pageParams: FindParams<M> = { ...findParams, limit: size };
|
|
232
|
-
if (cursorField) {
|
|
233
|
-
if (seeking) {
|
|
234
|
-
pageParams.where = appendCondition<M>(baseWhere, cursorField, [seekOp, cursorValue]);
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
pageParams.offset = offset;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const page = await find(pageParams);
|
|
241
|
-
pages += 1;
|
|
242
|
-
|
|
243
|
-
const rows = page?.data ?? [];
|
|
244
|
-
// A page with nothing on it always ends the walk, whatever the server
|
|
245
|
-
// claims about `hasMore` — there is no cursor to advance and no offset
|
|
246
|
-
// that would ever move past it.
|
|
247
|
-
if (rows.length === 0) return;
|
|
248
|
-
|
|
249
|
-
for (const row of rows) {
|
|
250
|
-
yield row;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// The server is the only authority on whether more rows exist. Never
|
|
254
|
-
// infer it from `rows.length >= size`: a last page that happens to be
|
|
255
|
-
// exactly full is indistinguishable from a middle one, and guessing
|
|
256
|
-
// there drops every row after it.
|
|
257
|
-
if (page?.meta?.hasMore !== true) return;
|
|
258
|
-
|
|
259
|
-
if (cursorField) {
|
|
260
|
-
const last = rows[rows.length - 1] as Record<string, unknown>;
|
|
261
|
-
const nextValue = last?.[cursorField];
|
|
262
|
-
if (nextValue === undefined || nextValue === null) {
|
|
263
|
-
throw new RebasePaginationError(
|
|
264
|
-
"cursor-missing",
|
|
265
|
-
`Cannot seek past the last row of "${label}": it has no value for the cursor ` +
|
|
266
|
-
`column "${cursorField}". Pick a column that is present and non-null on every row.`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (seeking && cursorEquals(nextValue, cursorValue)) {
|
|
270
|
-
throw new RebasePaginationError(
|
|
271
|
-
"cursor-stalled",
|
|
272
|
-
`Iterating "${label}" is stuck: two pages in a row ended at ` +
|
|
273
|
-
`${cursorField}=${String(nextValue)}. The cursor column has to be unique — a ` +
|
|
274
|
-
`repeated value cannot be seeked past, and continuing would either loop forever ` +
|
|
275
|
-
`or skip the duplicates. Use the primary key, or page by offset.`
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
cursorValue = nextValue;
|
|
279
|
-
seeking = true;
|
|
280
|
-
} else {
|
|
281
|
-
// Advance by what actually arrived, not by the page size: a server
|
|
282
|
-
// free to return fewer rows than asked for would otherwise leave a
|
|
283
|
-
// hole in the walk.
|
|
284
|
-
offset += rows.length;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* {@link paginateFind}, collected into an array under a ceiling.
|
|
291
|
-
*
|
|
292
|
-
* See {@link SDKCollectionClient.findAll}.
|
|
293
|
-
*/
|
|
294
|
-
export async function collectAllPages<M extends Record<string, unknown> = Record<string, unknown>>(
|
|
295
|
-
find: PageFinder<M>,
|
|
296
|
-
params?: FindAllParams<M>,
|
|
297
|
-
label = "collection"
|
|
298
|
-
): Promise<M[]> {
|
|
299
|
-
const { maxRows, ...rest } = (params ?? {}) as FindAllParams<M> & Record<string, unknown>;
|
|
300
|
-
const cap = normalizeMaxRows(maxRows as number | undefined);
|
|
301
|
-
|
|
302
|
-
const out: M[] = [];
|
|
303
|
-
for await (const row of paginateFind<M>(find, rest as IterateParams<M>, label)) {
|
|
304
|
-
out.push(row);
|
|
305
|
-
if (out.length > cap) {
|
|
306
|
-
throw new RebasePaginationError(
|
|
307
|
-
"max-rows",
|
|
308
|
-
`findAll("${label}") matched more than ${cap} rows. Returning the first ${cap} would ` +
|
|
309
|
-
`look like the whole answer and quietly not be one, so this throws instead. Raise ` +
|
|
310
|
-
`\`maxRows\` if you meant to load them all, or stream with \`iterate()\`.`
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return out;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Build the `iterate` / `findAll` pair for one collection from its `find`.
|
|
319
|
-
*
|
|
320
|
-
* Both transports call this, which is what keeps the two implementations from
|
|
321
|
-
* being two implementations.
|
|
322
|
-
*/
|
|
323
|
-
export function createPaginationHelpers<M extends Record<string, unknown> = Record<string, unknown>>(
|
|
324
|
-
find: PageFinder<M>,
|
|
325
|
-
label: string
|
|
326
|
-
): {
|
|
327
|
-
iterate: (params?: IterateParams<M>) => AsyncIterableIterator<M>;
|
|
328
|
-
findAll: (params?: FindAllParams<M>) => Promise<M[]>;
|
|
329
|
-
} {
|
|
330
|
-
return {
|
|
331
|
-
iterate: (params?: IterateParams<M>) => paginateFind<M>(find, params, label),
|
|
332
|
-
findAll: (params?: FindAllParams<M>) => collectAllPages<M>(find, params, label)
|
|
333
|
-
};
|
|
334
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CollectionAccessor,
|
|
3
|
-
FilterCondition,
|
|
4
|
-
FindParams,
|
|
5
|
-
FindResponse,
|
|
6
|
-
LogicalCondition,
|
|
7
|
-
OrderByTuple,
|
|
8
|
-
QueryBuilderInterface,
|
|
9
|
-
WhereFilterOp,
|
|
10
|
-
WhereValueFor,
|
|
11
|
-
type ComputedSortField
|
|
12
|
-
} from "@rebasepro/types";
|
|
13
|
-
import { normalizeOrderBy } from "./sort-dialect";
|
|
14
|
-
|
|
15
|
-
export function or(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
|
|
16
|
-
return { type: "or",
|
|
17
|
-
conditions };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function and(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
|
|
21
|
-
return { type: "and",
|
|
22
|
-
conditions };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function cond(column: string, operator: WhereFilterOp, value: unknown): FilterCondition {
|
|
26
|
-
return { column,
|
|
27
|
-
operator,
|
|
28
|
-
value };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {
|
|
32
|
-
// Keyed by plain `string` on purpose: it is written in place by the
|
|
33
|
-
// methods below, whose own parameters are typed against `M`, and a
|
|
34
|
-
// `Partial<Record<FieldPath<M>, …>>` is read-only under a generic `M`
|
|
35
|
-
// (TS2862). The typing users see is on the methods; this is the buffer
|
|
36
|
-
// behind them, cast once at each handoff.
|
|
37
|
-
private params: FindParams = { where: {} };
|
|
38
|
-
|
|
39
|
-
constructor(private collection: CollectionAccessor<M>) {}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Add a filter condition to your query.
|
|
43
|
-
* @example
|
|
44
|
-
* client.collection('users').where('age', '>=', 18).find()
|
|
45
|
-
*/
|
|
46
|
-
where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
|
|
47
|
-
where(logicalCondition: LogicalCondition): this;
|
|
48
|
-
where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown): this {
|
|
49
|
-
// Handle LogicalCondition signature
|
|
50
|
-
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
51
|
-
this.params.logical = columnOrCondition as LogicalCondition;
|
|
52
|
-
return this;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!this.params.where) {
|
|
56
|
-
this.params.where = {};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const column = columnOrCondition as string;
|
|
60
|
-
const condition: [WhereFilterOp, unknown] = [operator!, value];
|
|
61
|
-
const existing = this.params.where[column];
|
|
62
|
-
|
|
63
|
-
if (existing === undefined) {
|
|
64
|
-
this.params.where[column] = condition;
|
|
65
|
-
} else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {
|
|
66
|
-
(this.params.where[column] as [WhereFilterOp, unknown][]).push(condition);
|
|
67
|
-
} else {
|
|
68
|
-
// Convert existing single tuple/value into array of tuples
|
|
69
|
-
let firstCondition: [WhereFilterOp, unknown];
|
|
70
|
-
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") {
|
|
71
|
-
firstCondition = existing as [WhereFilterOp, unknown];
|
|
72
|
-
} else {
|
|
73
|
-
firstCondition = ["==", existing];
|
|
74
|
-
}
|
|
75
|
-
this.params.where[column] = [firstCondition, condition];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return this;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Order the results by a specific column.
|
|
83
|
-
*
|
|
84
|
-
* Called again, this adds a tie-breaker rather than replacing the sort:
|
|
85
|
-
* keys apply in the order they were added.
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
89
|
-
* @example
|
|
90
|
-
* client.collection('users').orderBy('roles').orderBy('createdAt', 'desc').find()
|
|
91
|
-
*/
|
|
92
|
-
orderBy(column: (keyof M & string) | ComputedSortField, direction: "asc" | "desc" = "asc"): this {
|
|
93
|
-
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
94
|
-
this.params.orderBy = [...existing, [column, direction] as OrderByTuple];
|
|
95
|
-
return this;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Limit the number of results returned.
|
|
100
|
-
*/
|
|
101
|
-
limit(count: number): this {
|
|
102
|
-
this.params.limit = count;
|
|
103
|
-
return this;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Skip the first N results.
|
|
108
|
-
*/
|
|
109
|
-
offset(count: number): this {
|
|
110
|
-
this.params.offset = count;
|
|
111
|
-
return this;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Set a free-text search string if supported by the backend.
|
|
116
|
-
*/
|
|
117
|
-
search(searchString: string, options?: { explain?: boolean }): this {
|
|
118
|
-
this.params.searchString = searchString;
|
|
119
|
-
if (options?.explain !== undefined) this.params.searchExplain = options.explain;
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
125
|
-
*
|
|
126
|
-
* Postgres only, over a property declared as `type: "vector"`. Rows come
|
|
127
|
-
* back with a `_distance`; `where` filters before the ordering.
|
|
128
|
-
*/
|
|
129
|
-
vectorSearch(
|
|
130
|
-
property: string,
|
|
131
|
-
vector: number[],
|
|
132
|
-
options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
|
|
133
|
-
): this {
|
|
134
|
-
this.params.vectorSearch = {
|
|
135
|
-
property,
|
|
136
|
-
vector,
|
|
137
|
-
...(options?.distance !== undefined && { distance: options.distance }),
|
|
138
|
-
...(options?.threshold !== undefined && { threshold: options.threshold })
|
|
139
|
-
};
|
|
140
|
-
return this;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Include related entities in the response.
|
|
145
|
-
* Relations will be populated with full entity data instead of just IDs.
|
|
146
|
-
*
|
|
147
|
-
* @param relations - Relation names to include, or "*" for all.
|
|
148
|
-
* @example
|
|
149
|
-
* // Include specific relations
|
|
150
|
-
* client.data.posts.include("tags", "author").find()
|
|
151
|
-
*
|
|
152
|
-
* // Include all relations
|
|
153
|
-
* client.data.posts.include("*").find()
|
|
154
|
-
*/
|
|
155
|
-
include(...relations: string[]): this {
|
|
156
|
-
this.params.include = relations;
|
|
157
|
-
return this;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Execute the find query and return the results.
|
|
162
|
-
*/
|
|
163
|
-
async find(): Promise<FindResponse<M>> {
|
|
164
|
-
return this.collection.find(this.params as FindParams<M>) as Promise<FindResponse<M>>;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Listen to realtime updates matching this query.
|
|
169
|
-
*/
|
|
170
|
-
listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void {
|
|
171
|
-
if (!this.collection.listen) {
|
|
172
|
-
throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
173
|
-
}
|
|
174
|
-
return this.collection.listen(this.params as FindParams<M>, onUpdate, onError);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DataSourceDefinition,
|
|
3
|
-
ResolvedDataSource,
|
|
4
|
-
DEFAULT_DATA_SOURCE_KEY,
|
|
5
|
-
getDataSourceCapabilities
|
|
6
|
-
} from "@rebasepro/types";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The subset of a collection needed to resolve its data source. Accepting a
|
|
10
|
-
* structural type (rather than the full `CollectionConfig`) keeps this usable
|
|
11
|
-
* from anywhere — frontend router, backend registry, editor — without coupling
|
|
12
|
-
* to the collection union.
|
|
13
|
-
*/
|
|
14
|
-
export interface DataSourceResolvable {
|
|
15
|
-
/** Preferred routing key. */
|
|
16
|
-
dataSource?: string;
|
|
17
|
-
/** Engine type discriminant (set on variant collection types). */
|
|
18
|
-
engine?: string;
|
|
19
|
-
/** Within-engine instance. */
|
|
20
|
-
databaseId?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** A lookup of data-source definitions by key. */
|
|
24
|
-
export type DataSourceRegistry = Record<string, DataSourceDefinition>;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
28
|
-
* Later entries win on key collision.
|
|
29
|
-
*/
|
|
30
|
-
export function createDataSourceRegistry(definitions?: DataSourceDefinition[]): DataSourceRegistry {
|
|
31
|
-
const registry: DataSourceRegistry = {};
|
|
32
|
-
for (const def of definitions ?? []) {
|
|
33
|
-
registry[def.key] = def;
|
|
34
|
-
}
|
|
35
|
-
return registry;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Resolve the effective data source for a collection — the single source of
|
|
40
|
-
* truth shared by the frontend router, the backend driver registry, and the
|
|
41
|
-
* editor's capability lookups.
|
|
42
|
-
*
|
|
43
|
-
* Resolution order:
|
|
44
|
-
* 1. The routing **key** is `collection.dataSource`, else
|
|
45
|
-
* {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
46
|
-
* 2. If a definition is registered for that key, it provides `engine`,
|
|
47
|
-
* `transport`, and `databaseId`.
|
|
48
|
-
* 3. Otherwise values are synthesized: `engine` from `collection.engine`
|
|
49
|
-
* (or the key, or `"postgres"`), `transport` defaults to `"server"`,
|
|
50
|
-
* and `databaseId` from the collection.
|
|
51
|
-
*
|
|
52
|
-
* `capabilities` are always derived from the resolved `engine`, so two
|
|
53
|
-
* data sources sharing an engine share capabilities.
|
|
54
|
-
*
|
|
55
|
-
* @param collection the collection (or any object carrying the routing fields)
|
|
56
|
-
* @param registry optional registry of declared data sources
|
|
57
|
-
*/
|
|
58
|
-
export function resolveDataSource(
|
|
59
|
-
collection: DataSourceResolvable | undefined,
|
|
60
|
-
registry?: DataSourceRegistry
|
|
61
|
-
): ResolvedDataSource {
|
|
62
|
-
const key = collection?.dataSource ?? DEFAULT_DATA_SOURCE_KEY;
|
|
63
|
-
const def = registry?.[key];
|
|
64
|
-
|
|
65
|
-
const engine = def?.engine
|
|
66
|
-
?? collection?.engine
|
|
67
|
-
?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
68
|
-
|
|
69
|
-
const transport = def?.transport ?? "server";
|
|
70
|
-
const databaseId = collection?.databaseId ?? def?.databaseId;
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
key,
|
|
74
|
-
engine,
|
|
75
|
-
transport,
|
|
76
|
-
databaseId,
|
|
77
|
-
capabilities: getDataSourceCapabilities(engine)
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Does a SQL toolchain own this collection's storage?
|
|
83
|
-
*
|
|
84
|
-
* "Owns the storage" means: something generates a table for it, pushes that
|
|
85
|
-
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
86
|
-
* the two disagree. That is true of a Postgres collection and false of a
|
|
87
|
-
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
88
|
-
* migrates — and the two were never told apart. Every stage of the SQL
|
|
89
|
-
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
90
|
-
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
91
|
-
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
92
|
-
* `db push` include list — where its name shielding a same-named real table
|
|
93
|
-
* from Atlas's exclude list is the one that can lose data.
|
|
94
|
-
*
|
|
95
|
-
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
96
|
-
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
97
|
-
* gets the same treatment as the built-in ones.
|
|
98
|
-
*
|
|
99
|
-
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
100
|
-
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
101
|
-
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
102
|
-
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
103
|
-
* including a collection generates a table nothing writes to; wrongly excluding
|
|
104
|
-
* one silently stops generating a table the app is serving from. Declare
|
|
105
|
-
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
106
|
-
*/
|
|
107
|
-
export function isRelationalCollection(
|
|
108
|
-
collection: DataSourceResolvable | undefined,
|
|
109
|
-
registry?: DataSourceRegistry
|
|
110
|
-
): boolean {
|
|
111
|
-
// The collection's own `engine` wins over a registered definition's. That
|
|
112
|
-
// is the opposite of {@link resolveDataSource}'s precedence, deliberately:
|
|
113
|
-
// there a definition describes where the data *goes*, so it should override;
|
|
114
|
-
// here the question is what the author said this collection is, and a
|
|
115
|
-
// collection declaring `engine: "firestore"` with no `dataSource` must not
|
|
116
|
-
// come back as the default source's engine and be handed a table.
|
|
117
|
-
const engine = collection?.engine
|
|
118
|
-
?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : undefined);
|
|
119
|
-
return getDataSourceCapabilities(engine).supportsRelations;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* The subset of `collections` a SQL toolchain owns — see
|
|
124
|
-
* {@link isRelationalCollection}.
|
|
125
|
-
*
|
|
126
|
-
* Every stage that generates SQL from collections starts by calling this, so
|
|
127
|
-
* the rule lives in one place rather than being re-decided per generator. It
|
|
128
|
-
* keeps the input order.
|
|
129
|
-
*/
|
|
130
|
-
export function relationalCollections<C extends DataSourceResolvable>(
|
|
131
|
-
collections: readonly C[],
|
|
132
|
-
registry?: DataSourceRegistry
|
|
133
|
-
): C[] {
|
|
134
|
-
return collections.filter(collection => isRelationalCollection(collection, registry));
|
|
135
|
-
}
|