@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.
Files changed (58) hide show
  1. package/README.md +4 -0
  2. package/dist/collections/CollectionRegistry.d.ts +1 -1
  3. package/dist/collections/default-collections.d.ts +15 -84
  4. package/dist/data/buildRebaseData.d.ts +1 -1
  5. package/dist/data/filter-dialect.d.ts +11 -0
  6. package/dist/data/sort-dialect.d.ts +15 -3
  7. package/dist/index.es.js +375 -63
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/util/builders.d.ts +69 -24
  10. package/dist/util/callback-errors.d.ts +77 -0
  11. package/dist/util/callback-errors.test.d.ts +1 -0
  12. package/dist/util/index.d.ts +1 -0
  13. package/dist/util/policy/evaluatePolicy.d.ts +6 -0
  14. package/dist/util/relations.d.ts +41 -0
  15. package/dist/util/table-name.test.d.ts +1 -0
  16. package/package.json +26 -22
  17. package/src/collections/CollectionRegistry.ts +0 -485
  18. package/src/collections/default-collections.ts +0 -109
  19. package/src/collections/index.ts +0 -2
  20. package/src/data/buildRebaseData.ts +0 -816
  21. package/src/data/buildRoutedRebaseData.ts +0 -103
  22. package/src/data/filter-conditions.ts +0 -46
  23. package/src/data/filter-dialect.ts +0 -737
  24. package/src/data/paginate.ts +0 -334
  25. package/src/data/query_builder.ts +0 -176
  26. package/src/data/resolveDataSource.ts +0 -135
  27. package/src/data/sort-dialect.ts +0 -237
  28. package/src/index.ts +0 -11
  29. package/src/table-classification.ts +0 -109
  30. package/src/types/json-logic-js.d.ts +0 -8
  31. package/src/util/auth-default-policies.ts +0 -215
  32. package/src/util/builders.ts +0 -82
  33. package/src/util/callbacks.ts +0 -122
  34. package/src/util/collections.ts +0 -117
  35. package/src/util/common.ts +0 -2
  36. package/src/util/conditions.ts +0 -168
  37. package/src/util/email.ts +0 -32
  38. package/src/util/entities.ts +0 -282
  39. package/src/util/enums.ts +0 -26
  40. package/src/util/identity.ts +0 -202
  41. package/src/util/index.ts +0 -21
  42. package/src/util/internal-tables.test.ts +0 -188
  43. package/src/util/internal-tables.ts +0 -197
  44. package/src/util/junction-policies.ts +0 -355
  45. package/src/util/paths.ts +0 -27
  46. package/src/util/permissions.test.ts +0 -866
  47. package/src/util/permissions.ts +0 -206
  48. package/src/util/pg-column-to-property.ts +0 -377
  49. package/src/util/policy/evaluatePolicy.ts +0 -194
  50. package/src/util/policy/index.ts +0 -4
  51. package/src/util/policy/policyToPostgres.ts +0 -263
  52. package/src/util/policy/securityRuleToConditions.ts +0 -67
  53. package/src/util/policy/sqlToPolicy.ts +0 -422
  54. package/src/util/relations.ts +0 -236
  55. package/src/util/resolutions.ts +0 -534
  56. package/src/util/resolve-relation.ts +0 -243
  57. package/src/util/storage.ts +0 -177
  58. package/src/util/string-column-length.ts +0 -31
@@ -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
- }