@rebasepro/common 0.8.0 → 0.9.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 -4
- package/dist/collections/CollectionRegistry.d.ts +16 -16
- package/dist/collections/default-collections.d.ts +1 -1
- package/dist/data/buildRebaseData.d.ts +30 -2
- package/dist/data/buildRoutedRebaseData.d.ts +14 -9
- package/dist/data/filter-dialect.d.ts +18 -4
- package/dist/data/query_builder.d.ts +1 -1
- package/dist/data/resolveDataSource.d.ts +1 -1
- package/dist/data/sort-dialect.d.ts +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +569 -159
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +573 -163
- package/dist/index.umd.js.map +1 -1
- package/dist/util/builders.d.ts +19 -56
- package/dist/util/callbacks.d.ts +3 -3
- package/dist/util/collections.d.ts +4 -4
- package/dist/util/entities.d.ts +2 -2
- package/dist/util/filter-operator-resolution.d.ts +32 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_from_path.d.ts +4 -4
- package/dist/util/navigation_utils.d.ts +3 -3
- package/dist/util/parent_references_from_path.d.ts +2 -2
- package/dist/util/permissions.d.ts +6 -6
- package/dist/util/policy/policyToPostgres.d.ts +14 -2
- package/dist/util/references.d.ts +2 -2
- package/dist/util/relations.d.ts +5 -5
- package/dist/util/resolutions.d.ts +2 -2
- package/package.json +3 -3
- package/src/collections/CollectionRegistry.ts +36 -36
- package/src/data/buildRebaseData.ts +332 -57
- package/src/data/buildRoutedRebaseData.ts +22 -16
- package/src/data/filter-dialect.ts +145 -60
- package/src/data/query_builder.ts +11 -2
- package/src/data/resolveDataSource.ts +1 -1
- package/src/data/sort-dialect.ts +56 -0
- package/src/index.ts +1 -0
- package/src/util/builders.ts +25 -99
- package/src/util/callbacks.ts +8 -8
- package/src/util/collections.ts +4 -4
- package/src/util/entities.ts +4 -4
- package/src/util/filter-operator-resolution.ts +81 -0
- package/src/util/index.ts +1 -0
- package/src/util/navigation_from_path.ts +4 -4
- package/src/util/navigation_utils.ts +8 -8
- package/src/util/parent_references_from_path.ts +3 -3
- package/src/util/permissions.test.ts +2 -2
- package/src/util/permissions.ts +7 -7
- package/src/util/policy/evaluatePolicy.ts +6 -0
- package/src/util/policy/policyToPostgres.ts +90 -10
- package/src/util/references.ts +2 -2
- package/src/util/relations.ts +12 -12
- package/src/util/resolutions.ts +5 -5
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
|
|
6
6
|
* Everything else speaks `FilterValues` exclusively.
|
|
7
7
|
*
|
|
8
|
+
* Wire-format values are always strings — the wire format carries no type
|
|
9
|
+
* metadata, so type coercion is the responsibility of the server-side data
|
|
10
|
+
* driver which has access to the collection schema.
|
|
11
|
+
*
|
|
12
|
+
* Commas inside list values are backslash-escaped (`\,`), and literal
|
|
13
|
+
* backslashes are escaped as `\\`.
|
|
14
|
+
*
|
|
8
15
|
* @module
|
|
9
16
|
*/
|
|
10
17
|
|
|
@@ -16,74 +23,124 @@ import {
|
|
|
16
23
|
RestFilterOp,
|
|
17
24
|
toCanonicalOp,
|
|
18
25
|
LogicalCondition,
|
|
19
|
-
FilterCondition
|
|
26
|
+
FilterCondition,
|
|
27
|
+
NULL_OPS
|
|
20
28
|
} from "@rebasepro/types";
|
|
21
29
|
|
|
22
30
|
// ---------------------------------------------------------------------------
|
|
23
|
-
// Value
|
|
31
|
+
// Value stringification
|
|
24
32
|
// ---------------------------------------------------------------------------
|
|
25
33
|
|
|
26
|
-
/**
|
|
27
|
-
* Coerce a raw querystring value to its natural JS type.
|
|
28
|
-
* - `"true"` / `"false"` → boolean
|
|
29
|
-
* - `"null"` → null
|
|
30
|
-
* - Numeric strings → number
|
|
31
|
-
* - Everything else → string (unchanged)
|
|
32
|
-
*/
|
|
33
|
-
function coerceValue(raw: string): unknown {
|
|
34
|
-
if (raw === "true") return true;
|
|
35
|
-
if (raw === "false") return false;
|
|
36
|
-
if (raw === "null") return null;
|
|
37
|
-
if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
|
|
38
|
-
return raw;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
34
|
/**
|
|
42
35
|
* Serialize a JS value to its querystring representation.
|
|
36
|
+
* `null` is serialized as the literal string `"null"`.
|
|
43
37
|
*/
|
|
44
38
|
function stringifyValue(value: unknown): string {
|
|
45
39
|
if (value === null) return "null";
|
|
46
|
-
if (typeof value === "boolean") return String(value);
|
|
47
40
|
return String(value);
|
|
48
41
|
}
|
|
49
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Comma escaping for list values
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Escape a single list item for the wire format.
|
|
49
|
+
* `\` → `\\`, `,` → `\,`
|
|
50
|
+
*/
|
|
51
|
+
function escapeListItem(value: string): string {
|
|
52
|
+
return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Unescape a single list item from the wire format.
|
|
57
|
+
* `\\` → `\`, `\,` → `,`
|
|
58
|
+
*/
|
|
59
|
+
function unescapeListItem(value: string): string {
|
|
60
|
+
let result = "";
|
|
61
|
+
for (let i = 0; i < value.length; i++) {
|
|
62
|
+
if (value[i] === "\\" && i + 1 < value.length) {
|
|
63
|
+
result += value[i + 1];
|
|
64
|
+
i++; // skip next char
|
|
65
|
+
} else {
|
|
66
|
+
result += value[i];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Split a parenthesized list string on unescaped commas.
|
|
74
|
+
* Input is the content between `(` and `)`.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* splitListItems("admin,editor") // ["admin", "editor"]
|
|
78
|
+
* splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
|
|
79
|
+
*/
|
|
80
|
+
function splitListItems(inner: string): string[] {
|
|
81
|
+
const items: string[] = [];
|
|
82
|
+
let current = "";
|
|
83
|
+
for (let i = 0; i < inner.length; i++) {
|
|
84
|
+
if (inner[i] === "\\" && i + 1 < inner.length) {
|
|
85
|
+
// Escaped character — consume both chars
|
|
86
|
+
current += inner[i] + inner[i + 1];
|
|
87
|
+
i++;
|
|
88
|
+
} else if (inner[i] === ",") {
|
|
89
|
+
items.push(unescapeListItem(current));
|
|
90
|
+
current = "";
|
|
91
|
+
} else {
|
|
92
|
+
current += inner[i];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
items.push(unescapeListItem(current));
|
|
96
|
+
return items;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Typed operator map lookups (no `as any`)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const REST_OP_LOOKUP = REST_TO_CANONICAL as Readonly<Record<string, WhereFilterOp | undefined>>;
|
|
104
|
+
const CANONICAL_OP_LOOKUP = CANONICAL_TO_REST as Readonly<Record<string, RestFilterOp | undefined>>;
|
|
105
|
+
|
|
50
106
|
// ---------------------------------------------------------------------------
|
|
51
107
|
// Serialize: FilterValues → REST querystring
|
|
52
108
|
// ---------------------------------------------------------------------------
|
|
53
109
|
|
|
54
110
|
/**
|
|
55
|
-
* Serialize a single condition tuple to a PostgREST dot-string.
|
|
111
|
+
* Serialize a single canonical condition tuple to a PostgREST dot-string.
|
|
112
|
+
*
|
|
113
|
+
* Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
|
|
56
114
|
*
|
|
57
115
|
* @example
|
|
58
116
|
* serializeTuple(["==", "active"]) // "eq.active"
|
|
59
117
|
* serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
|
|
60
118
|
* serializeTuple([">=", 18]) // "gte.18"
|
|
61
119
|
*/
|
|
62
|
-
function serializeTuple(tuple: [WhereFilterOp, unknown]
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (tuple.includes(".")) {
|
|
68
|
-
const dotIndex = tuple.indexOf(".");
|
|
69
|
-
const prefix = tuple.substring(0, dotIndex);
|
|
70
|
-
if ((REST_TO_CANONICAL as any)[prefix]) {
|
|
71
|
-
return tuple;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return tuple;
|
|
120
|
+
function serializeTuple(tuple: [WhereFilterOp, unknown]): string {
|
|
121
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) {
|
|
122
|
+
throw new TypeError(
|
|
123
|
+
`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`
|
|
124
|
+
);
|
|
75
125
|
}
|
|
76
126
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
const [op, value] = tuple;
|
|
128
|
+
|
|
129
|
+
if (typeof op !== "string") {
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
`serializeTuple: operator must be a string, got ${typeof op}`
|
|
132
|
+
);
|
|
80
133
|
}
|
|
81
134
|
|
|
82
|
-
const
|
|
83
|
-
|
|
135
|
+
const restOp = CANONICAL_OP_LOOKUP[op];
|
|
136
|
+
if (!restOp) {
|
|
137
|
+
throw new TypeError(
|
|
138
|
+
`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(CANONICAL_TO_REST).join(", ")}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
84
141
|
|
|
85
142
|
if (Array.isArray(value)) {
|
|
86
|
-
const items = value.map(stringifyValue).join(",");
|
|
143
|
+
const items = value.map(v => escapeListItem(stringifyValue(v))).join(",");
|
|
87
144
|
return `${restOp}.(${items})`;
|
|
88
145
|
}
|
|
89
146
|
|
|
@@ -91,8 +148,11 @@ function serializeTuple(tuple: [WhereFilterOp, unknown] | unknown): string {
|
|
|
91
148
|
}
|
|
92
149
|
|
|
93
150
|
/**
|
|
94
|
-
* Convert `FilterValues` to a PostgREST-style
|
|
151
|
+
* Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
|
|
152
|
+
* querystring record.
|
|
95
153
|
*
|
|
154
|
+
* - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
|
|
155
|
+
* - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
|
|
96
156
|
* - Single conditions produce a string value.
|
|
97
157
|
* - Multiple conditions on the same field produce a string array (repeated params).
|
|
98
158
|
*
|
|
@@ -102,22 +162,34 @@ function serializeTuple(tuple: [WhereFilterOp, unknown] | unknown): string {
|
|
|
102
162
|
*
|
|
103
163
|
* serializeFilter({ age: [[">=", 18], ["<", 65]] })
|
|
104
164
|
* // → { age: ["gte.18", "lt.65"] }
|
|
165
|
+
*
|
|
166
|
+
* // Pre-serialized strings pass through unchanged:
|
|
167
|
+
* serializeFilter({ status: "eq.published" })
|
|
168
|
+
* // → { status: "eq.published" }
|
|
105
169
|
*/
|
|
106
170
|
export function serializeFilter(
|
|
107
|
-
filter: FilterValues<string> | Record<string,
|
|
171
|
+
filter: FilterValues<string> | Record<string, unknown>
|
|
108
172
|
): Record<string, string | string[]> {
|
|
109
173
|
const result: Record<string, string | string[]> = {};
|
|
110
174
|
|
|
111
175
|
for (const [field, condition] of Object.entries(filter)) {
|
|
112
176
|
if (condition === undefined) continue;
|
|
113
177
|
|
|
178
|
+
// Pre-serialized PostgREST string — pass through unchanged.
|
|
179
|
+
// This supports WireFilterValues where values may already be
|
|
180
|
+
// serialized dot-strings like "eq.active" or raw strings like "true".
|
|
181
|
+
if (typeof condition === "string") {
|
|
182
|
+
result[field] = condition;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
114
186
|
// Multiple conditions on the same field: array of tuples
|
|
115
187
|
// We detect this by checking if the first element is also an array.
|
|
116
188
|
if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) {
|
|
117
|
-
result[field] = (condition as
|
|
189
|
+
result[field] = (condition as [WhereFilterOp, unknown][]).map(serializeTuple);
|
|
118
190
|
} else {
|
|
119
|
-
// Single condition
|
|
120
|
-
result[field] = serializeTuple(condition);
|
|
191
|
+
// Single condition — must be a [WhereFilterOp, value] tuple
|
|
192
|
+
result[field] = serializeTuple(condition as [WhereFilterOp, unknown]);
|
|
121
193
|
}
|
|
122
194
|
}
|
|
123
195
|
|
|
@@ -131,34 +203,47 @@ export function serializeFilter(
|
|
|
131
203
|
/**
|
|
132
204
|
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
133
205
|
*
|
|
134
|
-
*
|
|
206
|
+
* All values are returned as strings — the wire format carries no type
|
|
207
|
+
* metadata, so coercion is the data driver's responsibility.
|
|
208
|
+
*
|
|
209
|
+
* If the string doesn't match a known operator prefix, it falls back to
|
|
135
210
|
* `["==", originalString]` (treating the whole string as an equality value).
|
|
211
|
+
* This intentional defense handles values like `"user@host.com"` or
|
|
212
|
+
* `"1.2.3"` that happen to contain dots.
|
|
136
213
|
*/
|
|
137
214
|
function deserializeSingle(raw: string): [WhereFilterOp, unknown] {
|
|
138
215
|
const dotIndex = raw.indexOf(".");
|
|
139
216
|
if (dotIndex === -1) {
|
|
140
|
-
// No dot → equality on the raw value (
|
|
141
|
-
return ["==",
|
|
217
|
+
// No dot → equality on the raw value (kept as string)
|
|
218
|
+
return ["==", raw];
|
|
142
219
|
}
|
|
143
220
|
|
|
144
221
|
const prefix = raw.substring(0, dotIndex);
|
|
145
222
|
const rest = raw.substring(dotIndex + 1);
|
|
146
223
|
|
|
147
|
-
// Check if the prefix is a known REST operator
|
|
148
|
-
|
|
224
|
+
// Check if the prefix is a known REST operator.
|
|
225
|
+
// This is the key defense against values like "eq.something" or "gt.foo"
|
|
226
|
+
// being misinterpreted — only known REST short-codes are treated as operators.
|
|
227
|
+
const canonicalOp = REST_OP_LOOKUP[prefix];
|
|
149
228
|
if (!canonicalOp) {
|
|
150
229
|
// Not a known operator (e.g., email "user@host.com" or version "1.2.3")
|
|
151
230
|
// Treat the entire string as an equality value
|
|
152
231
|
return ["==", raw];
|
|
153
232
|
}
|
|
154
233
|
|
|
234
|
+
// Null-testing operators ignore their serialized value — normalize to null
|
|
235
|
+
// so the tuple round-trips stably (`isnull.null` → ["is-null", null]).
|
|
236
|
+
if (NULL_OPS.has(canonicalOp)) {
|
|
237
|
+
return [canonicalOp, null];
|
|
238
|
+
}
|
|
239
|
+
|
|
155
240
|
// Parse list values: "(admin,editor)" → ["admin", "editor"]
|
|
156
241
|
if (rest.startsWith("(") && rest.endsWith(")")) {
|
|
157
|
-
const items = rest.slice(1, -1)
|
|
242
|
+
const items = splitListItems(rest.slice(1, -1));
|
|
158
243
|
return [canonicalOp, items];
|
|
159
244
|
}
|
|
160
245
|
|
|
161
|
-
return [canonicalOp,
|
|
246
|
+
return [canonicalOp, rest];
|
|
162
247
|
}
|
|
163
248
|
|
|
164
249
|
/**
|
|
@@ -172,10 +257,10 @@ function deserializeSingle(raw: string): [WhereFilterOp, unknown] {
|
|
|
172
257
|
* // → { status: ["==", "active"] }
|
|
173
258
|
*
|
|
174
259
|
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
175
|
-
* // → { age: [[">=", 18], ["<", 65]] }
|
|
260
|
+
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
176
261
|
*/
|
|
177
262
|
export function deserializeFilter(
|
|
178
|
-
query: Record<string,
|
|
263
|
+
query: Record<string, unknown>
|
|
179
264
|
): FilterValues<string> {
|
|
180
265
|
const result: FilterValues<string> = {};
|
|
181
266
|
|
|
@@ -244,9 +329,9 @@ export function serializeLogicalCondition(
|
|
|
244
329
|
}
|
|
245
330
|
|
|
246
331
|
// FilterCondition
|
|
247
|
-
const restOp =
|
|
332
|
+
const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
|
|
248
333
|
if (Array.isArray(cond.value)) {
|
|
249
|
-
const items = cond.value.map(stringifyValue).join(",");
|
|
334
|
+
const items = cond.value.map(v => escapeListItem(stringifyValue(v))).join(",");
|
|
250
335
|
return `${cond.column}.${restOp}.(${items})`;
|
|
251
336
|
}
|
|
252
337
|
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
@@ -300,19 +385,19 @@ export function deserializeLogicalCondition(
|
|
|
300
385
|
|
|
301
386
|
const secondDot = rest.indexOf(".");
|
|
302
387
|
if (secondDot === -1) {
|
|
303
|
-
// "column.value" — treat as equality
|
|
304
|
-
return { column, operator: "==", value:
|
|
388
|
+
// "column.value" — treat as equality (value kept as string)
|
|
389
|
+
return { column, operator: "==", value: rest };
|
|
305
390
|
}
|
|
306
391
|
|
|
307
392
|
const opStr = rest.substring(0, secondDot);
|
|
308
|
-
|
|
393
|
+
const valueStr = rest.substring(secondDot + 1);
|
|
309
394
|
const operator = toCanonicalOp(opStr) ?? "==";
|
|
310
395
|
|
|
311
|
-
// Parse list values
|
|
396
|
+
// Parse list values with escape-aware splitting
|
|
312
397
|
if (valueStr.startsWith("(") && valueStr.endsWith(")")) {
|
|
313
|
-
const items = valueStr.slice(1, -1)
|
|
398
|
+
const items = splitListItems(valueStr.slice(1, -1));
|
|
314
399
|
return { column, operator, value: items };
|
|
315
400
|
}
|
|
316
401
|
|
|
317
|
-
return { column, operator, value:
|
|
402
|
+
return { column, operator, value: valueStr };
|
|
318
403
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CollectionAccessor,
|
|
3
|
+
FilterCondition,
|
|
4
|
+
FindParams,
|
|
5
|
+
FindResponse,
|
|
6
|
+
LogicalCondition,
|
|
7
|
+
QueryBuilderInterface,
|
|
8
|
+
WhereFilterOp,
|
|
9
|
+
WhereValue
|
|
10
|
+
} from "@rebasepro/types";
|
|
2
11
|
|
|
3
12
|
export function or(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
|
|
4
13
|
return { type: "or",
|
|
@@ -67,7 +76,7 @@ export class QueryBuilder<M extends Record<string, unknown> = Record<string, unk
|
|
|
67
76
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
68
77
|
*/
|
|
69
78
|
orderBy(column: keyof M & string, direction: "asc" | "desc" = "asc"): this {
|
|
70
|
-
this.params.orderBy =
|
|
79
|
+
this.params.orderBy = [column, direction];
|
|
71
80
|
return this;
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The subset of a collection needed to resolve its data source. Accepting a
|
|
10
|
-
* structural type (rather than the full `
|
|
10
|
+
* structural type (rather than the full `CollectionConfig`) keeps this usable
|
|
11
11
|
* from anywhere — frontend router, backend registry, editor — without coupling
|
|
12
12
|
* to the collection union.
|
|
13
13
|
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { OrderByTuple } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort-order wire codec.
|
|
5
|
+
*
|
|
6
|
+
* This is the ONLY module that knows about the colon-delimited wire format
|
|
7
|
+
* (`"field:direction"`) used in HTTP query parameters.
|
|
8
|
+
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
|
|
17
|
+
*
|
|
18
|
+
* **Runtime tolerance:** if the input is already a well-formed wire string
|
|
19
|
+
* (from an untyped JS caller), it is returned unchanged.
|
|
20
|
+
* This is undocumented tolerance, not public API — don't rely on it.
|
|
21
|
+
*
|
|
22
|
+
* @param orderBy - A canonical `[field, direction]` tuple, or at runtime
|
|
23
|
+
* possibly a pre-serialized string (undocumented tolerance).
|
|
24
|
+
* @returns The wire-format string, or `undefined` if the input is falsy.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Field names containing `:` are representable in the tuple form but
|
|
28
|
+
* **not** on the wire — this is an inherent limitation of the colon-delimited
|
|
29
|
+
* encoding and is not resolved here.
|
|
30
|
+
*/
|
|
31
|
+
export function serializeOrderBy(orderBy?: OrderByTuple | string): string | undefined {
|
|
32
|
+
if (!orderBy) return undefined;
|
|
33
|
+
// Runtime tolerance: pass through a pre-serialized wire string unchanged.
|
|
34
|
+
if (typeof orderBy === "string") return orderBy;
|
|
35
|
+
return `${orderBy[0]}:${orderBy[1]}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
|
|
40
|
+
*
|
|
41
|
+
* Lenient parsing (matches existing server behaviour):
|
|
42
|
+
* - Bare field name (no colon): `"name"` → `["name", "asc"]`
|
|
43
|
+
* - Unknown direction: `"name:foo"` → `["name", "asc"]`
|
|
44
|
+
* - Empty / falsy input: → `undefined`
|
|
45
|
+
*
|
|
46
|
+
* @param raw - The wire-format string from an HTTP query parameter.
|
|
47
|
+
* @returns The canonical tuple, or `undefined` if the input is empty/falsy.
|
|
48
|
+
*/
|
|
49
|
+
export function deserializeOrderBy(raw?: string): OrderByTuple | undefined {
|
|
50
|
+
if (!raw) return undefined;
|
|
51
|
+
const idx = raw.indexOf(":");
|
|
52
|
+
if (idx === -1) return [raw, "asc"];
|
|
53
|
+
const field = raw.slice(0, idx);
|
|
54
|
+
const dir = raw.slice(idx + 1);
|
|
55
|
+
return [field, dir === "desc" ? "desc" : "asc"];
|
|
56
|
+
}
|
package/src/index.ts
CHANGED
package/src/util/builders.ts
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AdditionalFieldDelegate,
|
|
3
2
|
ArrayProperty,
|
|
4
3
|
BooleanProperty,
|
|
5
4
|
DateProperty,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
EnumValueConfig,
|
|
9
|
-
EnumValues,
|
|
10
|
-
FirebaseCollection,
|
|
5
|
+
CollectionConfig,
|
|
6
|
+
FirebaseCollectionConfig,
|
|
11
7
|
FirebaseProperties,
|
|
12
8
|
GeopointProperty,
|
|
13
9
|
InferEntityType,
|
|
14
10
|
MapProperty,
|
|
15
|
-
|
|
11
|
+
MongoDBCollectionConfig,
|
|
16
12
|
MongoProperties,
|
|
17
13
|
NumberProperty,
|
|
18
|
-
|
|
14
|
+
PostgresCollectionConfig,
|
|
19
15
|
PostgresProperties,
|
|
20
|
-
Properties,
|
|
21
16
|
Property,
|
|
22
17
|
ReferenceProperty,
|
|
23
18
|
StringProperty,
|
|
@@ -26,17 +21,20 @@ import {
|
|
|
26
21
|
|
|
27
22
|
|
|
28
23
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
24
|
+
* @deprecated Use {@link defineCollection} instead — it infers property
|
|
25
|
+
* types automatically (autocomplete on `titleProperty`, `sort`,
|
|
26
|
+
* `propertiesOrder`, callbacks) without manual generics.
|
|
27
|
+
* `buildCollection` is kept for FireCMS migration compatibility and will
|
|
28
|
+
* be removed before 1.0.
|
|
29
|
+
*
|
|
32
30
|
* @group Builder
|
|
33
31
|
*/
|
|
34
32
|
export function buildCollection<
|
|
35
33
|
M extends Record<string, unknown> = Record<string, unknown>,
|
|
36
34
|
USER extends User = User>
|
|
37
35
|
(
|
|
38
|
-
collection:
|
|
39
|
-
):
|
|
36
|
+
collection: CollectionConfig<M, USER>
|
|
37
|
+
): CollectionConfig<M, USER> {
|
|
40
38
|
return collection;
|
|
41
39
|
}
|
|
42
40
|
|
|
@@ -74,8 +72,8 @@ export function defineCollection<
|
|
|
74
72
|
const P extends PostgresProperties,
|
|
75
73
|
USER extends User = User
|
|
76
74
|
>(
|
|
77
|
-
collection: Omit<
|
|
78
|
-
):
|
|
75
|
+
collection: Omit<PostgresCollectionConfig<InferEntityType<P>, USER>, "properties"> & { properties: P }
|
|
76
|
+
): PostgresCollectionConfig<InferEntityType<P>, USER> & { properties: P };
|
|
79
77
|
|
|
80
78
|
/**
|
|
81
79
|
* Define a Firestore-backed collection with full type inference.
|
|
@@ -85,8 +83,8 @@ export function defineCollection<
|
|
|
85
83
|
const P extends FirebaseProperties,
|
|
86
84
|
USER extends User = User
|
|
87
85
|
>(
|
|
88
|
-
collection: Omit<
|
|
89
|
-
):
|
|
86
|
+
collection: Omit<FirebaseCollectionConfig<InferEntityType<P>, USER>, "properties"> & { properties: P }
|
|
87
|
+
): FirebaseCollectionConfig<InferEntityType<P>, USER> & { properties: P };
|
|
90
88
|
|
|
91
89
|
/**
|
|
92
90
|
* Define a MongoDB-backed collection with full type inference.
|
|
@@ -96,23 +94,25 @@ export function defineCollection<
|
|
|
96
94
|
const P extends MongoProperties,
|
|
97
95
|
USER extends User = User
|
|
98
96
|
>(
|
|
99
|
-
collection: Omit<
|
|
100
|
-
):
|
|
97
|
+
collection: Omit<MongoDBCollectionConfig<InferEntityType<P>, USER>, "properties"> & { properties: P }
|
|
98
|
+
): MongoDBCollectionConfig<InferEntityType<P>, USER> & { properties: P };
|
|
101
99
|
|
|
102
100
|
/**
|
|
103
101
|
* Implementation — delegates to the correct overload at the type level.
|
|
104
102
|
* At runtime this is a plain identity function.
|
|
105
103
|
*/
|
|
106
104
|
export function defineCollection(
|
|
107
|
-
collection:
|
|
108
|
-
):
|
|
105
|
+
collection: CollectionConfig
|
|
106
|
+
): CollectionConfig {
|
|
109
107
|
return collection;
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
111
|
+
* @deprecated Use plain typed property objects with {@link defineCollection}
|
|
112
|
+
* instead — `defineCollection` infers property types automatically, making
|
|
113
|
+
* this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
|
|
114
|
+
* compatibility and will be removed before 1.0.
|
|
115
|
+
*
|
|
116
116
|
* @group Builder
|
|
117
117
|
*/
|
|
118
118
|
export function buildProperty<T, P extends Property = Property>(
|
|
@@ -130,77 +130,3 @@ export function buildProperty<T, P extends Property = Property>(
|
|
|
130
130
|
// SAFETY: Identity function — P is a subtype of the conditional return type by definition
|
|
131
131
|
return property as unknown as ReturnType<typeof buildProperty<T, P>>;
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
136
|
-
* the properties keys.
|
|
137
|
-
* @param properties
|
|
138
|
-
* @group Builder
|
|
139
|
-
*/
|
|
140
|
-
export function buildProperties<M extends Record<string, unknown>>(
|
|
141
|
-
properties: Properties
|
|
142
|
-
): Properties {
|
|
143
|
-
return properties;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
148
|
-
* the properties keys.
|
|
149
|
-
* @param propertiesOrBuilder
|
|
150
|
-
* @group Builder
|
|
151
|
-
*/
|
|
152
|
-
export function buildPropertiesOrBuilder<M extends Record<string, unknown>>(
|
|
153
|
-
propertiesOrBuilder: Properties
|
|
154
|
-
): Properties {
|
|
155
|
-
return propertiesOrBuilder;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
160
|
-
* the properties keys.
|
|
161
|
-
* @param enumValues
|
|
162
|
-
* @group Builder
|
|
163
|
-
*/
|
|
164
|
-
export function buildEnum(
|
|
165
|
-
enumValues: EnumValues
|
|
166
|
-
): EnumValues {
|
|
167
|
-
return enumValues;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
172
|
-
* the properties keys.
|
|
173
|
-
* @param enumValueConfig
|
|
174
|
-
* @group Builder
|
|
175
|
-
*/
|
|
176
|
-
export function buildEnumValueConfig(
|
|
177
|
-
enumValueConfig: EnumValueConfig
|
|
178
|
-
): EnumValueConfig {
|
|
179
|
-
return enumValueConfig;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
184
|
-
* the properties keys.
|
|
185
|
-
* @param callbacks
|
|
186
|
-
* @group Builder
|
|
187
|
-
*/
|
|
188
|
-
export function buildEntityCallbacks<M extends Record<string, unknown> = Record<string, unknown>>(
|
|
189
|
-
callbacks: EntityCallbacks<M>
|
|
190
|
-
): EntityCallbacks<M> {
|
|
191
|
-
return callbacks;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Identity function we use to defeat the type system of Typescript and build
|
|
196
|
-
* additional field delegates views with all its properties
|
|
197
|
-
* @param additionalFieldDelegate
|
|
198
|
-
* @group Builder
|
|
199
|
-
*/
|
|
200
|
-
export function buildAdditionalFieldDelegate<M extends Record<string, unknown>, USER extends User = User>(
|
|
201
|
-
additionalFieldDelegate: AdditionalFieldDelegate<M, USER>
|
|
202
|
-
): AdditionalFieldDelegate<M, USER> {
|
|
203
|
-
return additionalFieldDelegate;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
package/src/util/callbacks.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CollectionCallbacks, Properties, RebaseCallContext } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Context passed to entity lifecycle callbacks.
|
|
@@ -85,24 +85,24 @@ async function processProperties(
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
88
|
-
* and wrap them into an
|
|
88
|
+
* and wrap them into an CollectionCallbacks object recursively.
|
|
89
89
|
*/
|
|
90
|
-
export const buildPropertyCallbacks = (properties: Properties):
|
|
90
|
+
export const buildPropertyCallbacks = (properties: Properties): CollectionCallbacks | undefined => {
|
|
91
91
|
if (!properties) return undefined;
|
|
92
92
|
|
|
93
|
-
const propertyCallbacks:
|
|
93
|
+
const propertyCallbacks: CollectionCallbacks = {};
|
|
94
94
|
|
|
95
95
|
if (hasPropertyCallbacks(properties, "afterRead")) {
|
|
96
96
|
propertyCallbacks.afterRead = async (props) => {
|
|
97
|
+
const row = props.row;
|
|
97
98
|
const processedValues = await processProperties(
|
|
98
99
|
properties,
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
row,
|
|
101
|
+
row,
|
|
101
102
|
props as unknown,
|
|
102
103
|
"afterRead"
|
|
103
104
|
);
|
|
104
|
-
return { ...props.
|
|
105
|
-
values: processedValues };
|
|
105
|
+
return { ...props.row, ...processedValues };
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|