@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,166 @@
|
|
|
1
|
+
import { parseRelationAggregateSort } from "@rebasepro/types";
|
|
2
|
+
//#region src/data/cursor.ts
|
|
3
|
+
/** A cursor that cannot be read at all — truncated, re-encoded, or invented. */
|
|
4
|
+
var CursorError = class CursorError extends Error {
|
|
5
|
+
code = "INVALID_CURSOR";
|
|
6
|
+
constructor(detail) {
|
|
7
|
+
super(`Invalid \`after\` cursor: ${detail}. Pass back the \`meta.nextCursor\` from the previous page unchanged — it is opaque and must not be built by hand.`);
|
|
8
|
+
this.name = "CursorError";
|
|
9
|
+
Object.setPrototypeOf(this, CursorError.prototype);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A cursor that reads fine but describes a different query.
|
|
14
|
+
*
|
|
15
|
+
* Separate from {@link CursorError} because the fix is different: this one is
|
|
16
|
+
* not a corrupt string, it is a correct cursor used against a sort it was not
|
|
17
|
+
* produced under. Seeking anyway would return rows in an order nobody asked
|
|
18
|
+
* for, and — worse — would look like it worked.
|
|
19
|
+
*/
|
|
20
|
+
var CursorMismatchError = class CursorMismatchError extends Error {
|
|
21
|
+
code = "CURSOR_ORDER_MISMATCH";
|
|
22
|
+
constructor(cursorKeys, queryKeys) {
|
|
23
|
+
super(`The \`after\` cursor was produced by a query ordered by ${cursorKeys.map((k) => `"${k}"`).join(", ") || "(nothing)"}, but this query orders by ${queryKeys.map((k) => `"${k}"`).join(", ") || "(nothing)"}. A cursor only continues the listing it came from — keep \`orderBy\` identical across pages, or drop \`after\` to start over.`);
|
|
24
|
+
this.name = "CursorMismatchError";
|
|
25
|
+
Object.setPrototypeOf(this, CursorMismatchError.prototype);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Tag for a value whose JSON round-trip would otherwise lose its type.
|
|
30
|
+
*
|
|
31
|
+
* A `timestamp` column comes back from the driver as a `Date`; JSON turns it
|
|
32
|
+
* into a string, and the string would then be compared against the column by
|
|
33
|
+
* whatever cast Postgres chose. Round-tripping it as a `Date` keeps the
|
|
34
|
+
* comparison the one the ORDER BY made.
|
|
35
|
+
*/
|
|
36
|
+
var DATE_TAG = "$date";
|
|
37
|
+
function encodeValue(value) {
|
|
38
|
+
if (value instanceof Date) return { [DATE_TAG]: value.toISOString() };
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function decodeValue(value) {
|
|
42
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
43
|
+
const tagged = value[DATE_TAG];
|
|
44
|
+
if (typeof tagged === "string") {
|
|
45
|
+
const date = new Date(tagged);
|
|
46
|
+
return Number.isNaN(date.getTime()) ? tagged : date;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
/** base64url, without depending on Node's Buffer (this package runs in browsers). */
|
|
52
|
+
function toBase64Url(text) {
|
|
53
|
+
const bytes = new TextEncoder().encode(text);
|
|
54
|
+
let binary = "";
|
|
55
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
56
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
57
|
+
}
|
|
58
|
+
function fromBase64Url(encoded) {
|
|
59
|
+
const padded = encoded.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - encoded.length % 4) % 4);
|
|
60
|
+
const binary = atob(padded);
|
|
61
|
+
const bytes = new Uint8Array(binary.length);
|
|
62
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
63
|
+
return new TextDecoder().decode(bytes);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Encode "everything strictly after this row, in this order".
|
|
67
|
+
*
|
|
68
|
+
* @param orderBy the sort keys the listing ran under, in order of significance
|
|
69
|
+
* @param row the last row served, which the next page picks up after
|
|
70
|
+
* @param id that row's id — the tiebreaker every keyset comparison ends on
|
|
71
|
+
* @returns the opaque cursor, or `undefined` when no cursor can describe the
|
|
72
|
+
* page. That is not a failure: a listing sorted by relevance has no stored
|
|
73
|
+
* value to compare a later page against (scores are computed per query and
|
|
74
|
+
* are not on the same scale between two of them), so it pages by offset and
|
|
75
|
+
* `meta.nextCursor` is simply absent.
|
|
76
|
+
*/
|
|
77
|
+
function encodeCursor(orderBy, row, id) {
|
|
78
|
+
if (id === void 0 || id === null) return void 0;
|
|
79
|
+
const keys = orderBy ?? [];
|
|
80
|
+
const values = {};
|
|
81
|
+
for (const [field] of keys) {
|
|
82
|
+
if (field in row) {
|
|
83
|
+
values[field] = encodeValue(row[field]);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (parseRelationAggregateSort(field)) continue;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
return toBase64Url(JSON.stringify({
|
|
90
|
+
k: keys,
|
|
91
|
+
v: values,
|
|
92
|
+
i: encodeValue(id)
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Read a cursor produced by {@link encodeCursor}.
|
|
97
|
+
*
|
|
98
|
+
* @throws {CursorError} when the string is not a cursor this codec wrote.
|
|
99
|
+
*/
|
|
100
|
+
function decodeCursor(raw) {
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(fromBase64Url(raw.trim()));
|
|
104
|
+
} catch {
|
|
105
|
+
throw new CursorError("it is not a cursor this API issued");
|
|
106
|
+
}
|
|
107
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new CursorError("it does not decode to a cursor");
|
|
108
|
+
const body = parsed;
|
|
109
|
+
if (!Array.isArray(body.k)) throw new CursorError("it carries no sort keys");
|
|
110
|
+
if (body.i === void 0) throw new CursorError("it carries no row id");
|
|
111
|
+
const orderBy = [];
|
|
112
|
+
for (const entry of body.k) {
|
|
113
|
+
if (!Array.isArray(entry) || typeof entry[0] !== "string") throw new CursorError("one of its sort keys is malformed");
|
|
114
|
+
const direction = entry[1] === "desc" ? "desc" : "asc";
|
|
115
|
+
orderBy.push(entry[2] === "first" || entry[2] === "last" ? [
|
|
116
|
+
entry[0],
|
|
117
|
+
direction,
|
|
118
|
+
entry[2]
|
|
119
|
+
] : [entry[0], direction]);
|
|
120
|
+
}
|
|
121
|
+
const rawValues = body.v && typeof body.v === "object" && !Array.isArray(body.v) ? body.v : {};
|
|
122
|
+
const values = {};
|
|
123
|
+
for (const [field, value] of Object.entries(rawValues)) values[field] = decodeValue(value);
|
|
124
|
+
return {
|
|
125
|
+
orderBy,
|
|
126
|
+
values,
|
|
127
|
+
id: decodeValue(body.i)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The `orderBy` a request should run under, given a cursor and whatever sort
|
|
132
|
+
* the request itself named.
|
|
133
|
+
*
|
|
134
|
+
* A request that names no sort **adopts the cursor's** — that is what makes
|
|
135
|
+
* `find({ after })` work without restating the `orderBy` from the previous
|
|
136
|
+
* call, and it cannot be wrong, since the cursor is the only sort in play.
|
|
137
|
+
* A request that names one must name the *same* one, key for key, direction for
|
|
138
|
+
* direction, nulls for nulls; anything else is {@link CursorMismatchError}.
|
|
139
|
+
*
|
|
140
|
+
* @throws {CursorMismatchError}
|
|
141
|
+
*/
|
|
142
|
+
function reconcileCursorOrder(cursor, requested) {
|
|
143
|
+
if (!requested || requested.length === 0) return cursor.orderBy;
|
|
144
|
+
const spell = (keys) => keys.map(([field, direction, nulls]) => `${field}:${direction}${nulls ? `:${nulls}` : ""}`);
|
|
145
|
+
const cursorKeys = spell(cursor.orderBy);
|
|
146
|
+
const queryKeys = spell(requested);
|
|
147
|
+
if (cursorKeys.length !== queryKeys.length || cursorKeys.some((key, i) => key !== queryKeys[i])) throw new CursorMismatchError(cursorKeys, queryKeys);
|
|
148
|
+
return requested;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The `startAfter` shape the driver contract takes, built from a cursor.
|
|
152
|
+
*
|
|
153
|
+
* The driver has always accepted `{ id, values }`; this is the one place that
|
|
154
|
+
* shape is produced, so the REST route and the WebSocket ingress cannot drift
|
|
155
|
+
* into two spellings of the same seek.
|
|
156
|
+
*/
|
|
157
|
+
function cursorToStartAfter(cursor) {
|
|
158
|
+
return {
|
|
159
|
+
id: cursor.id,
|
|
160
|
+
values: cursor.values
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { CursorError, CursorMismatchError, cursorToStartAfter, decodeCursor, encodeCursor, reconcileCursorOrder };
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=cursor.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.es.js","names":[],"sources":["../../src/data/cursor.ts"],"sourcesContent":["import { parseRelationAggregateSort, type OrderByTuple } from \"@rebasepro/types\";\n\n/**\n * The keyset-cursor wire codec.\n *\n * ## Why this is one module\n *\n * Keyset pagination was implemented three times and reachable once. The driver\n * has a NULL-correct multi-key comparison (`FetchService.buildKeysetComparison`)\n * that only a WebSocket `startAfter` could reach; REST could not seek at all;\n * and the SDK's `iterate({cursor})` re-implemented a *single*-column keyset as a\n * `where` clause, which threw on any multi-key sort and silently dropped rows\n * whose sort value was NULL. Three implementations, three answers to \"what is\n * page two\".\n *\n * There is now one. The driver's comparison is the implementation; this module\n * is the only thing that says how a cursor is written down, and every transport\n * — the REST `?after=`, the WebSocket `startAfter`, the SDK's `iterate()` —\n * carries the string this produces and hands it back unread.\n *\n * ## What a cursor holds\n *\n * The sort keys the query was ordered by, the last served row's value for each\n * of them, and that row's id. The keys travel *with* the values because a\n * cursor that carried only values would be silently reinterpretable: paging a\n * `created_at DESC` listing and then asking for `title ASC` would seek on the\n * dates as though they were titles. Carrying the keys makes that a refusal\n * ({@link CursorMismatchError}) rather than a page of arbitrary rows.\n *\n * ## Opacity\n *\n * The encoding is base64url of JSON, and it is **not** API. It is opaque so it\n * can change — adding a key, changing how a value is tagged — without every\n * client that learned to read it breaking. Nothing outside this file parses it.\n *\n * @module\n */\n\n/** The decoded contents of a cursor. */\nexport interface DecodedCursor {\n /** The sort keys the cursor was produced under, in order of significance. */\n orderBy: OrderByTuple[];\n /** The last served row's value for each sort key, by field name. */\n values: Record<string, unknown>;\n /** The last served row's id, which breaks ties on the last key. */\n id: unknown;\n}\n\n/** A cursor that cannot be read at all — truncated, re-encoded, or invented. */\nexport class CursorError extends Error {\n readonly code = \"INVALID_CURSOR\";\n constructor(detail: string) {\n super(\n `Invalid \\`after\\` cursor: ${detail}. Pass back the \\`meta.nextCursor\\` ` +\n \"from the previous page unchanged — it is opaque and must not be built by hand.\"\n );\n this.name = \"CursorError\";\n Object.setPrototypeOf(this, CursorError.prototype);\n }\n}\n\n/**\n * A cursor that reads fine but describes a different query.\n *\n * Separate from {@link CursorError} because the fix is different: this one is\n * not a corrupt string, it is a correct cursor used against a sort it was not\n * produced under. Seeking anyway would return rows in an order nobody asked\n * for, and — worse — would look like it worked.\n */\nexport class CursorMismatchError extends Error {\n readonly code = \"CURSOR_ORDER_MISMATCH\";\n constructor(cursorKeys: string[], queryKeys: string[]) {\n super(\n `The \\`after\\` cursor was produced by a query ordered by ` +\n `${cursorKeys.map(k => `\"${k}\"`).join(\", \") || \"(nothing)\"}, but this query orders by ` +\n `${queryKeys.map(k => `\"${k}\"`).join(\", \") || \"(nothing)\"}. A cursor only continues the ` +\n \"listing it came from — keep `orderBy` identical across pages, or drop `after` to start over.\"\n );\n this.name = \"CursorMismatchError\";\n Object.setPrototypeOf(this, CursorMismatchError.prototype);\n }\n}\n\n/**\n * Tag for a value whose JSON round-trip would otherwise lose its type.\n *\n * A `timestamp` column comes back from the driver as a `Date`; JSON turns it\n * into a string, and the string would then be compared against the column by\n * whatever cast Postgres chose. Round-tripping it as a `Date` keeps the\n * comparison the one the ORDER BY made.\n */\nconst DATE_TAG = \"$date\";\n\nfunction encodeValue(value: unknown): unknown {\n if (value instanceof Date) return { [DATE_TAG]: value.toISOString() };\n return value;\n}\n\nfunction decodeValue(value: unknown): unknown {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const tagged = (value as Record<string, unknown>)[DATE_TAG];\n if (typeof tagged === \"string\") {\n const date = new Date(tagged);\n return Number.isNaN(date.getTime()) ? tagged : date;\n }\n }\n return value;\n}\n\n/** base64url, without depending on Node's Buffer (this package runs in browsers). */\nfunction toBase64Url(text: string): string {\n const bytes = new TextEncoder().encode(text);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction fromBase64Url(encoded: string): string {\n const padded = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\")\n + \"=\".repeat((4 - (encoded.length % 4)) % 4);\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return new TextDecoder().decode(bytes);\n}\n\n/**\n * Encode \"everything strictly after this row, in this order\".\n *\n * @param orderBy the sort keys the listing ran under, in order of significance\n * @param row the last row served, which the next page picks up after\n * @param id that row's id — the tiebreaker every keyset comparison ends on\n * @returns the opaque cursor, or `undefined` when no cursor can describe the\n * page. That is not a failure: a listing sorted by relevance has no stored\n * value to compare a later page against (scores are computed per query and\n * are not on the same scale between two of them), so it pages by offset and\n * `meta.nextCursor` is simply absent.\n */\nexport function encodeCursor(\n orderBy: OrderByTuple[] | undefined,\n row: Record<string, unknown>,\n id: unknown\n): string | undefined {\n if (id === undefined || id === null) return undefined;\n const keys = orderBy ?? [];\n // A key whose value is not on the row cannot be seeked past. Rather than\n // emit a cursor that the next request would refuse, emit none — the caller\n // falls back to offset paging, which is what it did before cursors existed.\n //\n // An aggregate over a relation (`count(orders)`) is the exception: it is\n // never on the row, and it needs no value here, because the keyset\n // comparison recomputes the cursor row's value in SQL from the id.\n // Refusing it withheld the cursor from every such listing.\n const values: Record<string, unknown> = {};\n for (const [field] of keys) {\n if (field in row) {\n values[field] = encodeValue(row[field]);\n continue;\n }\n if (parseRelationAggregateSort(field)) continue;\n return undefined;\n }\n return toBase64Url(JSON.stringify({ k: keys, v: values, i: encodeValue(id) }));\n}\n\n/**\n * Read a cursor produced by {@link encodeCursor}.\n *\n * @throws {CursorError} when the string is not a cursor this codec wrote.\n */\nexport function decodeCursor(raw: string): DecodedCursor {\n let parsed: unknown;\n try {\n parsed = JSON.parse(fromBase64Url(raw.trim()));\n } catch {\n throw new CursorError(\"it is not a cursor this API issued\");\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new CursorError(\"it does not decode to a cursor\");\n }\n const body = parsed as { k?: unknown; v?: unknown; i?: unknown };\n if (!Array.isArray(body.k)) throw new CursorError(\"it carries no sort keys\");\n if (body.i === undefined) throw new CursorError(\"it carries no row id\");\n\n const orderBy: OrderByTuple[] = [];\n for (const entry of body.k) {\n if (!Array.isArray(entry) || typeof entry[0] !== \"string\") {\n throw new CursorError(\"one of its sort keys is malformed\");\n }\n const direction = entry[1] === \"desc\" ? \"desc\" : \"asc\";\n orderBy.push(entry[2] === \"first\" || entry[2] === \"last\"\n ? [entry[0], direction, entry[2]]\n : [entry[0], direction]);\n }\n\n const rawValues = (body.v && typeof body.v === \"object\" && !Array.isArray(body.v))\n ? body.v as Record<string, unknown>\n : {};\n const values: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(rawValues)) values[field] = decodeValue(value);\n\n return { orderBy, values, id: decodeValue(body.i) };\n}\n\n/**\n * The `orderBy` a request should run under, given a cursor and whatever sort\n * the request itself named.\n *\n * A request that names no sort **adopts the cursor's** — that is what makes\n * `find({ after })` work without restating the `orderBy` from the previous\n * call, and it cannot be wrong, since the cursor is the only sort in play.\n * A request that names one must name the *same* one, key for key, direction for\n * direction, nulls for nulls; anything else is {@link CursorMismatchError}.\n *\n * @throws {CursorMismatchError}\n */\nexport function reconcileCursorOrder(\n cursor: DecodedCursor,\n requested: OrderByTuple[] | undefined\n): OrderByTuple[] {\n if (!requested || requested.length === 0) return cursor.orderBy;\n const spell = (keys: OrderByTuple[]) =>\n keys.map(([field, direction, nulls]) => `${field}:${direction}${nulls ? `:${nulls}` : \"\"}`);\n const cursorKeys = spell(cursor.orderBy);\n const queryKeys = spell(requested);\n if (cursorKeys.length !== queryKeys.length\n || cursorKeys.some((key, i) => key !== queryKeys[i])) {\n throw new CursorMismatchError(cursorKeys, queryKeys);\n }\n return requested;\n}\n\n/**\n * The `startAfter` shape the driver contract takes, built from a cursor.\n *\n * The driver has always accepted `{ id, values }`; this is the one place that\n * shape is produced, so the REST route and the WebSocket ingress cannot drift\n * into two spellings of the same seek.\n */\nexport function cursorToStartAfter(cursor: DecodedCursor): Record<string, unknown> {\n return { id: cursor.id, values: cursor.values };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/data/filter-conditions.ts
|
|
2
|
+
/**
|
|
3
|
+
* Read one field's filter as the list of conditions it stands for.
|
|
4
|
+
*
|
|
5
|
+
* Accepts both declared shapes and normalises them to a list:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* toFilterTuples(["==", "active"]) // [["==", "active"]]
|
|
9
|
+
* toFilterTuples([[">=", 18], ["<", 65]]) // [[">=", 18], ["<", 65]]
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* A falsy, non-array or empty param has no conditions in it — the empty list,
|
|
13
|
+
* so a caller iterating adds nothing rather than compiling a tuple of
|
|
14
|
+
* `undefined`s and logging about an operator nobody sent.
|
|
15
|
+
*/
|
|
16
|
+
function toFilterTuples(filterParam) {
|
|
17
|
+
if (!filterParam || !Array.isArray(filterParam) || filterParam.length === 0) return [];
|
|
18
|
+
if (Array.isArray(filterParam[0])) return filterParam;
|
|
19
|
+
return [filterParam];
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { toFilterTuples };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=filter-conditions.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-conditions.es.js","names":[],"sources":["../../src/data/filter-conditions.ts"],"sourcesContent":["/**\n * The `FilterValues` grammar, one level below the wire codec.\n *\n * A field's filter is either one `[op, value]` tuple or an **array** of them —\n * `{ age: [[\">=\", 18], [\"<\", 65]] }` — which is what the fluent builder produces\n * from two `.where()` calls on the same column. Reading that shape is grammar,\n * not a driver detail, so every compiler reads it through here.\n *\n * It lived only inside the Postgres compiler, and the Mongo one destructured\n * `const [op, value] = filterParam` regardless: given the array-of-tuples form\n * `op` bound to `[\">=\", 18]`, no operator matched, and the condition was\n * dropped. Both of them. A read asking for adults under 65 returned every row\n * of the collection with a 200.\n *\n * @module\n */\n\nimport type { WhereFilterOp } from \"@rebasepro/types\";\n\n/** One `[operator, value]` condition. */\nexport type FilterTuple = [WhereFilterOp, unknown];\n\n/**\n * Read one field's filter as the list of conditions it stands for.\n *\n * Accepts both declared shapes and normalises them to a list:\n *\n * ```ts\n * toFilterTuples([\"==\", \"active\"]) // [[\"==\", \"active\"]]\n * toFilterTuples([[\">=\", 18], [\"<\", 65]]) // [[\">=\", 18], [\"<\", 65]]\n * ```\n *\n * A falsy, non-array or empty param has no conditions in it — the empty list,\n * so a caller iterating adds nothing rather than compiling a tuple of\n * `undefined`s and logging about an operator nobody sent.\n */\nexport function toFilterTuples(filterParam: unknown): FilterTuple[] {\n if (!filterParam || !Array.isArray(filterParam) || filterParam.length === 0) return [];\n // The first element discriminates: a condition starts with an operator\n // string, a list of conditions starts with a condition. `[\"in\", [\"a\",\"b\"]]`\n // is one condition whose value happens to be a list.\n if (Array.isArray(filterParam[0])) {\n return filterParam as FilterTuple[];\n }\n return [filterParam as FilterTuple];\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoCA,SAAgB,eAAe,aAAqC;CAChE,IAAI,CAAC,eAAe,CAAC,MAAM,QAAQ,WAAW,KAAK,YAAY,WAAW,GAAG,OAAO,CAAC;CAIrF,IAAI,MAAM,QAAQ,YAAY,EAAE,GAC5B,OAAO;CAEX,OAAO,CAAC,WAA0B;AACtC"}
|
|
@@ -66,9 +66,9 @@ export declare class UnknownFilterOperatorError extends Error {
|
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* The query-parameter names a REST listing reads as something other than a
|
|
69
|
-
* field filter: pagination, sorting, includes,
|
|
70
|
-
* groups, the JSON `where` dialect, an
|
|
71
|
-
* soft-delete switches.
|
|
69
|
+
* field filter: pagination and whether to count, sorting, includes,
|
|
70
|
+
* projection, search, the logical groups, the JSON `where` dialect, an
|
|
71
|
+
* aggregate's `select`/`groupBy`, and the soft-delete switches.
|
|
72
72
|
*
|
|
73
73
|
* One list for both ends. The server skips these names when it collects
|
|
74
74
|
* `?<field>=<op>.<value>` filters, and the SDK sends a filter on a column with
|