@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.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/collections/CollectionRegistry.d.ts +16 -16
  3. package/dist/collections/default-collections.d.ts +1 -1
  4. package/dist/data/buildRebaseData.d.ts +30 -2
  5. package/dist/data/buildRoutedRebaseData.d.ts +14 -9
  6. package/dist/data/filter-dialect.d.ts +18 -4
  7. package/dist/data/query_builder.d.ts +1 -1
  8. package/dist/data/resolveDataSource.d.ts +1 -1
  9. package/dist/data/sort-dialect.d.ts +41 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.es.js +569 -159
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.umd.js +573 -163
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/util/builders.d.ts +19 -56
  16. package/dist/util/callbacks.d.ts +3 -3
  17. package/dist/util/collections.d.ts +4 -4
  18. package/dist/util/entities.d.ts +2 -2
  19. package/dist/util/filter-operator-resolution.d.ts +32 -0
  20. package/dist/util/index.d.ts +1 -0
  21. package/dist/util/navigation_from_path.d.ts +4 -4
  22. package/dist/util/navigation_utils.d.ts +3 -3
  23. package/dist/util/parent_references_from_path.d.ts +2 -2
  24. package/dist/util/permissions.d.ts +6 -6
  25. package/dist/util/policy/policyToPostgres.d.ts +14 -2
  26. package/dist/util/references.d.ts +2 -2
  27. package/dist/util/relations.d.ts +5 -5
  28. package/dist/util/resolutions.d.ts +2 -2
  29. package/package.json +3 -3
  30. package/src/collections/CollectionRegistry.ts +36 -36
  31. package/src/data/buildRebaseData.ts +332 -57
  32. package/src/data/buildRoutedRebaseData.ts +22 -16
  33. package/src/data/filter-dialect.ts +145 -60
  34. package/src/data/query_builder.ts +11 -2
  35. package/src/data/resolveDataSource.ts +1 -1
  36. package/src/data/sort-dialect.ts +56 -0
  37. package/src/index.ts +1 -0
  38. package/src/util/builders.ts +25 -99
  39. package/src/util/callbacks.ts +8 -8
  40. package/src/util/collections.ts +4 -4
  41. package/src/util/entities.ts +4 -4
  42. package/src/util/filter-operator-resolution.ts +81 -0
  43. package/src/util/index.ts +1 -0
  44. package/src/util/navigation_from_path.ts +4 -4
  45. package/src/util/navigation_utils.ts +8 -8
  46. package/src/util/parent_references_from_path.ts +3 -3
  47. package/src/util/permissions.test.ts +2 -2
  48. package/src/util/permissions.ts +7 -7
  49. package/src/util/policy/evaluatePolicy.ts +6 -0
  50. package/src/util/policy/policyToPostgres.ts +90 -10
  51. package/src/util/references.ts +2 -2
  52. package/src/util/relations.ts +12 -12
  53. 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 coercion (querystring → typed JS values)
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] | unknown): string {
63
- // If it's already a string, it might be a PostgREST string (with dot)
64
- // or a raw value (without dot). In both cases, existing tests expect
65
- // them to be passed through or treated as simple equality if no dot.
66
- if (typeof tuple === "string") {
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
- // If it's NOT a canonical tuple [WhereFilterOp, value], treat as equality.
78
- if (!Array.isArray(tuple) || tuple.length !== 2 || typeof tuple[0] !== "string" || !(CANONICAL_TO_REST as any)[tuple[0]]) {
79
- return `eq.${stringifyValue(tuple)}`;
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 [op, value] = tuple as [WhereFilterOp, unknown];
83
- const restOp = CANONICAL_TO_REST[op];
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 querystring record.
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, any>
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 any[]).map(serializeTuple);
189
+ result[field] = (condition as [WhereFilterOp, unknown][]).map(serializeTuple);
118
190
  } else {
119
- // Single condition (could be a tuple, a raw value, or an already-serialized string)
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
- * If the string doesn't match a known operator prefix, falls back to
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 (coerced)
141
- return ["==", coerceValue(raw)];
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
- const canonicalOp = (REST_TO_CANONICAL as Record<string, WhereFilterOp | undefined>)[prefix];
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).split(",").map(s => coerceValue(s.trim()));
242
+ const items = splitListItems(rest.slice(1, -1));
158
243
  return [canonicalOp, items];
159
244
  }
160
245
 
161
- return [canonicalOp, coerceValue(rest)];
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, any>
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 = (CANONICAL_TO_REST as any)[cond.operator] || "eq";
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: coerceValue(rest) };
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
- let valueStr = rest.substring(secondDot + 1);
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).split(",").map(s => coerceValue(s.trim()));
398
+ const items = splitListItems(valueStr.slice(1, -1));
314
399
  return { column, operator, value: items };
315
400
  }
316
401
 
317
- return { column, operator, value: coerceValue(valueStr) };
402
+ return { column, operator, value: valueStr };
318
403
  }
@@ -1,4 +1,13 @@
1
- import { FindParams, Entity, FindResponse, CollectionAccessor, QueryBuilderInterface, WhereFilterOp, LogicalCondition, WhereValue, FilterCondition } from "@rebasepro/types";
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 = `${column}:${direction}`;
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 `EntityCollection`) keeps this usable
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
@@ -5,4 +5,5 @@ export * from "./data/buildRoutedRebaseData";
5
5
  export * from "./data/resolveDataSource";
6
6
  export * from "./data/query_builder";
7
7
  export * from "./data/filter-dialect";
8
+ export * from "./data/sort-dialect";
8
9
  export * from "./table-classification";
@@ -1,23 +1,18 @@
1
1
  import {
2
- AdditionalFieldDelegate,
3
2
  ArrayProperty,
4
3
  BooleanProperty,
5
4
  DateProperty,
6
- EntityCallbacks,
7
- EntityCollection,
8
- EnumValueConfig,
9
- EnumValues,
10
- FirebaseCollection,
5
+ CollectionConfig,
6
+ FirebaseCollectionConfig,
11
7
  FirebaseProperties,
12
8
  GeopointProperty,
13
9
  InferEntityType,
14
10
  MapProperty,
15
- MongoDBCollection,
11
+ MongoDBCollectionConfig,
16
12
  MongoProperties,
17
13
  NumberProperty,
18
- PostgresCollection,
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
- * Identity function we use to defeat the type system of Typescript and build
30
- * collection views with all its properties
31
- * @param collection
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: EntityCollection<M, USER>
39
- ): EntityCollection<M, USER> {
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<PostgresCollection<InferEntityType<P>, USER>, "properties"> & { properties: P }
78
- ): PostgresCollection<InferEntityType<P>, USER> & { properties: P };
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<FirebaseCollection<InferEntityType<P>, USER>, "properties"> & { properties: P }
89
- ): FirebaseCollection<InferEntityType<P>, USER> & { properties: P };
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<MongoDBCollection<InferEntityType<P>, USER>, "properties"> & { properties: P }
100
- ): MongoDBCollection<InferEntityType<P>, USER> & { properties: P };
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: EntityCollection
108
- ): EntityCollection {
105
+ collection: CollectionConfig
106
+ ): CollectionConfig {
109
107
  return collection;
110
108
  }
111
109
 
112
110
  /**
113
- * Identity function we use to defeat the type system of Typescript and preserve
114
- * the property keys.
115
- * @param property
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
-
@@ -1,4 +1,4 @@
1
- import { EntityCallbacks, Properties, RebaseCallContext } from "@rebasepro/types";
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 EntityCallbacks object recursively.
88
+ * and wrap them into an CollectionCallbacks object recursively.
89
89
  */
90
- export const buildPropertyCallbacks = (properties: Properties): EntityCallbacks | undefined => {
90
+ export const buildPropertyCallbacks = (properties: Properties): CollectionCallbacks | undefined => {
91
91
  if (!properties) return undefined;
92
92
 
93
- const propertyCallbacks: EntityCallbacks = {};
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
- props.entity.values as Record<string, unknown>,
100
- props.entity.values as Record<string, unknown>,
100
+ row,
101
+ row,
101
102
  props as unknown,
102
103
  "afterRead"
103
104
  );
104
- return { ...props.entity,
105
- values: processedValues };
105
+ return { ...props.row, ...processedValues };
106
106
  };
107
107
  }
108
108