@rebasepro/common 0.13.0 → 0.13.1-canary.g3660bd5
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/data/paginate.d.ts +20 -0
- package/dist/index.es.js +44 -15
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +2 -2
- package/package.json +3 -3
- package/src/data/buildRebaseData.ts +37 -14
- package/src/data/paginate.ts +30 -0
- package/src/util/builders.ts +3 -3
- package/src/util/resolutions.ts +5 -5
package/dist/util/builders.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { FirebaseCollectionConfig, FirebaseProperties, InferEntityType, MongoDBC
|
|
|
3
3
|
* Define a PostgreSQL-backed collection with full type inference.
|
|
4
4
|
*
|
|
5
5
|
* The `const P` generic captures literal property types from your
|
|
6
|
-
* `properties` object, which enables autocomplete on `
|
|
6
|
+
* `properties` object, which enables autocomplete on `display.title`,
|
|
7
7
|
* `sort`, `propertiesOrder`, `fixedFilter`, and entity callbacks.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
@@ -16,7 +16,7 @@ import { FirebaseCollectionConfig, FirebaseProperties, InferEntityType, MongoDBC
|
|
|
16
16
|
* name: { name: "Name", type: "string", validation: { required: true } },
|
|
17
17
|
* price: { name: "Price", type: "number" },
|
|
18
18
|
* },
|
|
19
|
-
*
|
|
19
|
+
* display: { title: "name" }, // ✅ autocomplete: "name" | "price"
|
|
20
20
|
* sort: ["price", "asc"], // ✅ autocomplete on first element
|
|
21
21
|
* });
|
|
22
22
|
* ```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.1-canary.g3660bd5",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"fast-equals": "6.0.2",
|
|
42
42
|
"json-logic-js": "^2.0.5",
|
|
43
|
-
"@rebasepro/types": "0.13.
|
|
44
|
-
"@rebasepro/utils": "0.13.
|
|
43
|
+
"@rebasepro/types": "0.13.1-canary.g3660bd5",
|
|
44
|
+
"@rebasepro/utils": "0.13.1-canary.g3660bd5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@jest/globals": "^30.4.1",
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "@rebasepro/types";
|
|
19
19
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
20
20
|
import { QueryBuilder } from "./query_builder";
|
|
21
|
-
import { collectAllPages, paginateFind } from "./paginate";
|
|
21
|
+
import { collectAllPages, paginateFind, resolveFindWindow } from "./paginate";
|
|
22
22
|
import { deserializeFilter } from "./filter-dialect";
|
|
23
23
|
import { buildCompositeId, resolvePrimaryKeys, PrimaryKeyInfo } from "../util/identity";
|
|
24
24
|
|
|
@@ -153,8 +153,7 @@ function createDriverAccessor<M extends Record<string, unknown> = Record<string,
|
|
|
153
153
|
async find(params?: FindParams<M>): Promise<FindResponse<M>> {
|
|
154
154
|
// Ensure filters are in canonical [op, value] format even if passed as PostgREST strings
|
|
155
155
|
const filter = params?.where ? deserializeFilter(params.where as Record<string, unknown>) : undefined;
|
|
156
|
-
const limit = params
|
|
157
|
-
const offset = params?.offset ?? 0;
|
|
156
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
158
157
|
|
|
159
158
|
// One relation shape, whatever the call looks like.
|
|
160
159
|
//
|
|
@@ -177,8 +176,12 @@ function createDriverAccessor<M extends Record<string, unknown> = Record<string,
|
|
|
177
176
|
slug,
|
|
178
177
|
{
|
|
179
178
|
filter,
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
// Without this the group was dropped and the read ran
|
|
180
|
+
// unfiltered — every row the caller's policies allow,
|
|
181
|
+
// in place of the ones they asked for.
|
|
182
|
+
logical: params?.logical,
|
|
183
|
+
limit,
|
|
184
|
+
offset: driverOffset,
|
|
182
185
|
orderBy: params?.orderBy?.[0],
|
|
183
186
|
order: params?.orderBy?.[1],
|
|
184
187
|
searchString: params?.searchString
|
|
@@ -187,9 +190,10 @@ function createDriverAccessor<M extends Record<string, unknown> = Record<string,
|
|
|
187
190
|
)
|
|
188
191
|
: await driver.fetchCollection<M>({
|
|
189
192
|
path: slug,
|
|
190
|
-
limit
|
|
191
|
-
offset:
|
|
193
|
+
limit,
|
|
194
|
+
offset: driverOffset,
|
|
192
195
|
filter,
|
|
196
|
+
logical: params?.logical,
|
|
193
197
|
orderBy: params?.orderBy?.[0],
|
|
194
198
|
order: params?.orderBy?.[1],
|
|
195
199
|
searchString: params?.searchString
|
|
@@ -199,7 +203,16 @@ function createDriverAccessor<M extends Record<string, unknown> = Record<string,
|
|
|
199
203
|
let total = rows.length + offset;
|
|
200
204
|
let hasMore = rows.length >= limit;
|
|
201
205
|
if (driver.count) {
|
|
202
|
-
|
|
206
|
+
// The same narrowing the rows were read with. Counting only by
|
|
207
|
+
// `filter` reported the whole collection beside a narrowed
|
|
208
|
+
// page, and `hasMore` is derived from it — so the list offered
|
|
209
|
+
// a next page that did not exist.
|
|
210
|
+
total = await driver.count({
|
|
211
|
+
path: slug,
|
|
212
|
+
filter,
|
|
213
|
+
logical: params?.logical,
|
|
214
|
+
searchString: params?.searchString
|
|
215
|
+
});
|
|
203
216
|
hasMore = offset + rows.length < total;
|
|
204
217
|
}
|
|
205
218
|
|
|
@@ -261,26 +274,31 @@ values: {} as Record<string, unknown> }
|
|
|
261
274
|
count: driver.count
|
|
262
275
|
? async (params?: FindParams<M>): Promise<number> => {
|
|
263
276
|
const filter = params?.where ? deserializeFilter(params.where as Record<string, unknown>) : undefined;
|
|
277
|
+
// Every narrowing `find()` applies has to apply here too, or
|
|
278
|
+
// the count describes a different query than the one it is
|
|
279
|
+
// reported against.
|
|
264
280
|
return driver.count!({
|
|
265
281
|
path: slug,
|
|
266
|
-
filter
|
|
282
|
+
filter,
|
|
283
|
+
logical: params?.logical,
|
|
284
|
+
searchString: params?.searchString
|
|
267
285
|
});
|
|
268
286
|
}
|
|
269
287
|
: undefined,
|
|
270
288
|
|
|
271
289
|
listen: driver.listenCollection
|
|
272
290
|
? (params: FindParams<M> | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void) => {
|
|
273
|
-
const limit = params
|
|
274
|
-
const offset = params?.offset ?? 0;
|
|
291
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
275
292
|
// Realtime has no REST-pipeline equivalent, so the rows arrive
|
|
276
293
|
// admin-shaped. Flatten them to the one shape the rest of this
|
|
277
294
|
// accessor serves.
|
|
278
295
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row: Record<string, unknown>) => row;
|
|
279
296
|
return driver.listenCollection!<M>({
|
|
280
297
|
path: slug,
|
|
281
|
-
limit
|
|
282
|
-
offset:
|
|
298
|
+
limit,
|
|
299
|
+
offset: driverOffset,
|
|
283
300
|
filter: params?.where,
|
|
301
|
+
logical: params?.logical,
|
|
284
302
|
orderBy: params?.orderBy?.[0],
|
|
285
303
|
order: params?.orderBy?.[1],
|
|
286
304
|
searchString: params?.searchString,
|
|
@@ -288,7 +306,12 @@ values: {} as Record<string, unknown> }
|
|
|
288
306
|
onUpdate({
|
|
289
307
|
data: entities.map((row: Record<string, unknown>) => rowToEntity<M>(normalize(row), slug, getPks())),
|
|
290
308
|
meta: {
|
|
291
|
-
|
|
309
|
+
// No count is issued on this path, so the total
|
|
310
|
+
// is unknown; the lower bound is the rows in
|
|
311
|
+
// hand plus the ones paged past to reach them.
|
|
312
|
+
// Reporting `entities.length` claimed a read at
|
|
313
|
+
// offset 100 had found a collection of two.
|
|
314
|
+
total: offset + entities.length,
|
|
292
315
|
limit,
|
|
293
316
|
offset,
|
|
294
317
|
hasMore: entities.length >= limit
|
package/src/data/paginate.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_LIST_LIMIT,
|
|
2
3
|
FilterValues,
|
|
3
4
|
FieldPath,
|
|
4
5
|
FindAllParams,
|
|
@@ -70,6 +71,35 @@ export class RebasePaginationError extends Error {
|
|
|
70
71
|
export type PageFinder<M extends Record<string, unknown> = Record<string, unknown>> =
|
|
71
72
|
(params: FindParams<M>) => Promise<FindResult<M>>;
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
76
|
+
*
|
|
77
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
78
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
79
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
80
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
81
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
82
|
+
* rows are the mildest of those outcomes.
|
|
83
|
+
*
|
|
84
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
85
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
86
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
87
|
+
* look like it is paging by offset.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveFindWindow(
|
|
90
|
+
params?: Pick<FindParams, "limit" | "offset" | "page">
|
|
91
|
+
): { limit: number; offset: number; driverOffset: number | undefined } {
|
|
92
|
+
const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
|
|
93
|
+
const offset = params?.page != null
|
|
94
|
+
? Math.max(0, (params.page - 1) * limit)
|
|
95
|
+
: (params?.offset ?? 0);
|
|
96
|
+
return {
|
|
97
|
+
limit,
|
|
98
|
+
offset,
|
|
99
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
function normalizePageSize(raw: number | undefined): number {
|
|
74
104
|
if (raw === undefined || !Number.isFinite(raw)) return DEFAULT_PAGE_SIZE;
|
|
75
105
|
return Math.max(1, Math.floor(raw));
|
package/src/util/builders.ts
CHANGED
|
@@ -14,14 +14,14 @@ import {
|
|
|
14
14
|
// ── defineCollection ─────────────────────────────────────────────────────
|
|
15
15
|
// A smarter builder that uses `const` type-parameter inference (TS 5.0+)
|
|
16
16
|
// to capture literal property types automatically. This gives you
|
|
17
|
-
// autocomplete on `
|
|
17
|
+
// autocomplete on `display.title`, `sort`, `propertiesOrder`, `fixedFilter`,
|
|
18
18
|
// callbacks, etc. — without writing `as const` or passing manual generics.
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Define a PostgreSQL-backed collection with full type inference.
|
|
22
22
|
*
|
|
23
23
|
* The `const P` generic captures literal property types from your
|
|
24
|
-
* `properties` object, which enables autocomplete on `
|
|
24
|
+
* `properties` object, which enables autocomplete on `display.title`,
|
|
25
25
|
* `sort`, `propertiesOrder`, `fixedFilter`, and entity callbacks.
|
|
26
26
|
*
|
|
27
27
|
* @example
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
* name: { name: "Name", type: "string", validation: { required: true } },
|
|
35
35
|
* price: { name: "Price", type: "number" },
|
|
36
36
|
* },
|
|
37
|
-
*
|
|
37
|
+
* display: { title: "name" }, // ✅ autocomplete: "name" | "price"
|
|
38
38
|
* sort: ["price", "asc"], // ✅ autocomplete on first element
|
|
39
39
|
* });
|
|
40
40
|
* ```
|
package/src/util/resolutions.ts
CHANGED
|
@@ -261,11 +261,11 @@ export function resolveArrayProperties<M>({
|
|
|
261
261
|
ignoreMissingFields,
|
|
262
262
|
...props
|
|
263
263
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
} = props;
|
|
264
|
+
// Destructured to be *excluded* from `...rest`, not to be used —
|
|
265
|
+
// see the comment below. Said explicitly so the discarded-value
|
|
266
|
+
// ratchet does not carry a finding that is working as intended.
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
268
|
+
const { values, previousValues, ...rest } = props;
|
|
269
269
|
const ofProperty = resolveProperty({ // we don't want to pass the values of the parent entity
|
|
270
270
|
property: of,
|
|
271
271
|
ignoreMissingFields,
|