@rebasepro/common 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -0
  2. package/dist/collections/CollectionRegistry.d.ts +1 -1
  3. package/dist/collections/default-collections.d.ts +15 -84
  4. package/dist/data/buildRebaseData.d.ts +1 -1
  5. package/dist/data/filter-dialect.d.ts +11 -0
  6. package/dist/data/sort-dialect.d.ts +15 -3
  7. package/dist/index.es.js +375 -63
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/util/builders.d.ts +69 -24
  10. package/dist/util/callback-errors.d.ts +77 -0
  11. package/dist/util/callback-errors.test.d.ts +1 -0
  12. package/dist/util/index.d.ts +1 -0
  13. package/dist/util/policy/evaluatePolicy.d.ts +6 -0
  14. package/dist/util/relations.d.ts +41 -0
  15. package/dist/util/table-name.test.d.ts +1 -0
  16. package/package.json +26 -22
  17. package/src/collections/CollectionRegistry.ts +0 -485
  18. package/src/collections/default-collections.ts +0 -109
  19. package/src/collections/index.ts +0 -2
  20. package/src/data/buildRebaseData.ts +0 -816
  21. package/src/data/buildRoutedRebaseData.ts +0 -103
  22. package/src/data/filter-conditions.ts +0 -46
  23. package/src/data/filter-dialect.ts +0 -737
  24. package/src/data/paginate.ts +0 -334
  25. package/src/data/query_builder.ts +0 -176
  26. package/src/data/resolveDataSource.ts +0 -135
  27. package/src/data/sort-dialect.ts +0 -237
  28. package/src/index.ts +0 -11
  29. package/src/table-classification.ts +0 -109
  30. package/src/types/json-logic-js.d.ts +0 -8
  31. package/src/util/auth-default-policies.ts +0 -215
  32. package/src/util/builders.ts +0 -82
  33. package/src/util/callbacks.ts +0 -122
  34. package/src/util/collections.ts +0 -117
  35. package/src/util/common.ts +0 -2
  36. package/src/util/conditions.ts +0 -168
  37. package/src/util/email.ts +0 -32
  38. package/src/util/entities.ts +0 -282
  39. package/src/util/enums.ts +0 -26
  40. package/src/util/identity.ts +0 -202
  41. package/src/util/index.ts +0 -21
  42. package/src/util/internal-tables.test.ts +0 -188
  43. package/src/util/internal-tables.ts +0 -197
  44. package/src/util/junction-policies.ts +0 -355
  45. package/src/util/paths.ts +0 -27
  46. package/src/util/permissions.test.ts +0 -866
  47. package/src/util/permissions.ts +0 -206
  48. package/src/util/pg-column-to-property.ts +0 -377
  49. package/src/util/policy/evaluatePolicy.ts +0 -194
  50. package/src/util/policy/index.ts +0 -4
  51. package/src/util/policy/policyToPostgres.ts +0 -263
  52. package/src/util/policy/securityRuleToConditions.ts +0 -67
  53. package/src/util/policy/sqlToPolicy.ts +0 -422
  54. package/src/util/relations.ts +0 -236
  55. package/src/util/resolutions.ts +0 -534
  56. package/src/util/resolve-relation.ts +0 -243
  57. package/src/util/storage.ts +0 -177
  58. package/src/util/string-column-length.ts +0 -31
@@ -1,122 +0,0 @@
1
- import { CollectionCallbacks, Properties, RebaseCallContext } from "@rebasepro/types";
2
-
3
- /**
4
- * Context passed to entity lifecycle callbacks.
5
- * @group Models
6
- */
7
- export type EntityCallbackContext = RebaseCallContext;
8
-
9
-
10
- /**
11
- * Helper function to recursively check if there are any callbacks in the properties.
12
- */
13
- function hasPropertyCallbacks(properties: Properties, callbackName: "afterRead" | "beforeSave"): boolean {
14
- if (!properties) return false;
15
- for (const property of Object.values(properties)) {
16
- if (property.callbacks?.[callbackName]) return true;
17
- if (property.type === "map" && property.properties) {
18
- if (hasPropertyCallbacks(property.properties, callbackName)) return true;
19
- } else if (property.type === "array" && property.of) {
20
- const ofs = Array.isArray(property.of) ? property.of : [property.of];
21
- for (const of of ofs) {
22
- if (of.callbacks?.[callbackName]) return true;
23
- if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
24
- }
25
- }
26
- }
27
- return false;
28
- }
29
-
30
- /**
31
- * Recursively process properties to apply field-level hooks.
32
- */
33
- async function processProperties(
34
- properties: Properties,
35
- values: Record<string, unknown>,
36
- previousValues: Record<string, unknown>,
37
- propsContext: unknown,
38
- callbackName: "afterRead" | "beforeSave"
39
- ): Promise<Record<string, unknown>> {
40
- if (!values || typeof values !== "object") return values;
41
-
42
- const result = { ...values };
43
-
44
- for (const [key, property] of Object.entries(properties)) {
45
- if (result[key] === undefined) continue;
46
-
47
- let currentValue = result[key];
48
- const previousValue = previousValues?.[key];
49
-
50
- // 1. Array Property
51
- if (property.type === "array" && Array.isArray(currentValue)) {
52
- // We only support traversing single-type arrays for hooks currently to avoid complex union matching
53
- if (property.of && !Array.isArray(property.of)) {
54
- currentValue = await Promise.all(currentValue.map(async (item, index) => {
55
- const prevItem = Array.isArray(previousValue) ? previousValue[index] : undefined;
56
- // Mock a properties object to process a single item
57
- const singlePropData = { "_tmp": property.of } as Properties;
58
- const res = await processProperties(singlePropData, { "_tmp": item }, { "_tmp": prevItem }, propsContext, callbackName);
59
- return res["_tmp"];
60
- }));
61
- }
62
- }
63
- // 2. Map Property
64
- else if (property.type === "map" && property.properties && typeof currentValue === "object") {
65
- currentValue = await processProperties(property.properties, currentValue as Record<string, unknown>, (previousValue ?? {}) as Record<string, unknown>, propsContext, callbackName);
66
- }
67
-
68
- // 3. Property's own callback
69
- if (property.callbacks?.[callbackName]) {
70
-
71
- const cbRes = await Promise.resolve(property.callbacks[callbackName]({
72
- ...(propsContext as Record<string, unknown>),
73
- value: currentValue,
74
- previousValue
75
- } as never));
76
- if (cbRes !== undefined) {
77
- currentValue = cbRes;
78
- }
79
- }
80
-
81
- result[key] = currentValue;
82
- }
83
- return result;
84
- }
85
-
86
- /**
87
- * Helper function to extract field-level PropertyCallbacks from a properties schema
88
- * and wrap them into an CollectionCallbacks object recursively.
89
- */
90
- export const buildPropertyCallbacks = (properties: Properties): CollectionCallbacks | undefined => {
91
- if (!properties) return undefined;
92
-
93
- const propertyCallbacks: CollectionCallbacks = {};
94
-
95
- if (hasPropertyCallbacks(properties, "afterRead")) {
96
- propertyCallbacks.afterRead = async (props) => {
97
- const row = props.row;
98
- const processedValues = await processProperties(
99
- properties,
100
- row,
101
- row,
102
- props as unknown,
103
- "afterRead"
104
- );
105
- return { ...props.row, ...processedValues };
106
- };
107
- }
108
-
109
- if (hasPropertyCallbacks(properties, "beforeSave")) {
110
- propertyCallbacks.beforeSave = async (props) => {
111
- return await processProperties(
112
- properties,
113
- props.values as Record<string, unknown>,
114
- (props.previousValues ?? {}) as Record<string, unknown>,
115
- props as unknown,
116
- "beforeSave"
117
- );
118
- };
119
- }
120
-
121
- return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : undefined;
122
- };
@@ -1,117 +0,0 @@
1
- import {
2
- CollectionConfig,
3
- Properties,
4
- Property
5
- } from "@rebasepro/types";
6
- import { isPropertyBuilder } from "./entities";
7
-
8
- export function sortProperties<M extends Record<string, unknown>>(properties: Properties, propertiesOrder?: string[]): Properties {
9
- try {
10
- const propertiesKeys = Object.keys(properties);
11
- // If no propertiesOrder, just use the original keys order
12
- if (!propertiesOrder || propertiesOrder.length === 0) {
13
- return propertiesKeys
14
- .map((key) => {
15
- const property = properties[key] as Property;
16
- if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
17
- return ({
18
- [key]: {
19
- ...property,
20
- properties: sortProperties(property.properties, property.propertiesOrder)
21
- }
22
- });
23
- } else {
24
- return ({ [key]: property });
25
- }
26
- })
27
- .reduce((a: Properties, b: Properties) => ({ ...a,
28
- ...b }), {}) as Properties;
29
- }
30
-
31
- // Filter propertiesOrder to only include TOP-LEVEL property keys that exist
32
- // (ignore nested keys like "data.mode" - they are for column ordering, not property filtering)
33
- const validOrderKeys = (propertiesOrder as string[]).filter(key => {
34
- // Only include top-level keys (no dots) that exist in properties
35
- return !key.includes(".") && properties[key];
36
- });
37
-
38
- // Track which properties we've processed
39
- const processedKeys = new Set<string>(validOrderKeys);
40
-
41
- // Build result starting with ordered properties
42
- const orderedResult = validOrderKeys
43
- .map((key) => {
44
- const property = properties[key] as Property;
45
- if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
46
- return ({
47
- [key]: {
48
- ...property,
49
- properties: sortProperties(property.properties, property.propertiesOrder)
50
- }
51
- });
52
- } else {
53
- return ({ [key]: property });
54
- }
55
- })
56
- .reduce((a: Properties, b: Properties) => ({ ...a,
57
- ...b }), {}) as Properties;
58
-
59
- // Append any properties that were NOT in propertiesOrder (so they don't disappear!)
60
- const missingProperties = propertiesKeys
61
- .filter(key => !processedKeys.has(key))
62
- .map((key) => {
63
- const property = properties[key] as Property;
64
- if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
65
- return ({
66
- [key]: {
67
- ...property,
68
- properties: sortProperties(property.properties, property.propertiesOrder)
69
- }
70
- });
71
- } else {
72
- return ({ [key]: property });
73
- }
74
- })
75
- .reduce((a: Properties, b: Properties) => ({ ...a,
76
- ...b }), {}) as Properties;
77
-
78
- return { ...orderedResult,
79
- ...missingProperties };
80
- } catch (e) {
81
- console.error("Error sorting properties", e);
82
- return properties;
83
- }
84
- }
85
-
86
- /**
87
- * A copy of `collections` ordered by slug.
88
- *
89
- * Every generator that turns collections into a file is order-dependent, and
90
- * every one of them is compared against its own output — `rebase doctor`
91
- * regenerates in memory and diffs, `generate-sdk && git diff --exit-code` gates
92
- * CI. While only the *writers* sorted, a project whose `readdirSync` order
93
- * differed from its slug order was reported permanently out of date, and the
94
- * fix the message printed rewrote the file in the order it was already in. The
95
- * generators sort themselves now, so no caller can get this wrong.
96
- *
97
- * A slug-less collection is left to the generator's own validation, which names
98
- * the offending collection; sorting must not throw first.
99
- */
100
- export function sortCollectionsBySlug<C extends { slug?: string }>(collections: readonly C[]): C[] {
101
- return [...collections].sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
102
- }
103
-
104
- export function getPrimaryKeys<M extends Record<string, unknown>>(collection: CollectionConfig<M>): Extract<keyof M, string>[] {
105
- const properties = collection.properties;
106
- if (!properties) {
107
- return ["id"] as Extract<keyof M, string>[];
108
- }
109
- const ids = Object.entries(properties)
110
- .filter(([key, prop]) => typeof prop === "object" && prop !== null && "isId" in prop && Boolean(prop.isId))
111
- .map(([key]) => key);
112
-
113
- if (ids.length > 0) {
114
- return ids as Extract<keyof M, string>[];
115
- }
116
- return ["id"] as Extract<keyof M, string>[];
117
- }
@@ -1,2 +0,0 @@
1
- export const DEFAULT_ONE_OF_TYPE = "type"
2
- export const DEFAULT_ONE_OF_VALUE = "value"
@@ -1,168 +0,0 @@
1
- import jsonLogic from "json-logic-js";
2
- import {
3
- ArrayProperty,
4
- AuthState,
5
- ConditionContext,
6
- ConditionRule,
7
- EnumValueConfig,
8
- NumberProperty,
9
- PropertyConditions,
10
- Property,
11
- ReferenceProperty,
12
- StringProperty
13
- } from "@rebasepro/types";
14
-
15
- /**
16
- * Access a nested property from an object via dot notation.
17
- */
18
- function getIn(obj: Record<string, unknown> | unknown, path: string): unknown {
19
- if (!obj || !path) return undefined;
20
- return path.split(".").reduce((acc: unknown, part: string) => acc && (acc as Record<string, unknown>)[part], obj);
21
- }
22
-
23
- let operationsRegistered = false;
24
-
25
- /**
26
- * Register custom JSON Logic operations for Rebase.
27
- * Call this once at app initialization.
28
- */
29
- export function registerConditionOperations(): void {
30
- if (operationsRegistered) return;
31
-
32
- // Check if user has a specific role by ID
33
- jsonLogic.add_operation("hasRole", function (this: ConditionContext, roleId: string) {
34
- return this?.user?.roles?.includes(roleId) ?? false;
35
- });
36
-
37
- // Check if user has any of the specified roles
38
- jsonLogic.add_operation("hasAnyRole", function (this: ConditionContext, roleIds: string[]) {
39
- if (!this?.user?.roles || !Array.isArray(roleIds)) return false;
40
- return roleIds.some(role => this.user.roles.includes(role));
41
- });
42
-
43
- // Check if a timestamp is today
44
- jsonLogic.add_operation("isToday", (timestamp: number) => {
45
- if (!timestamp) return false;
46
- const date = new Date(timestamp);
47
- const today = new Date();
48
- return date.getFullYear() === today.getFullYear() &&
49
- date.getMonth() === today.getMonth() &&
50
- date.getDate() === today.getDate();
51
- });
52
-
53
- // Check if a timestamp is in the past
54
- jsonLogic.add_operation("isPast", (timestamp: number) => {
55
- if (!timestamp) return false;
56
- return timestamp < Date.now();
57
- });
58
-
59
- // Check if a timestamp is in the future
60
- jsonLogic.add_operation("isFuture", (timestamp: number) => {
61
- if (!timestamp) return false;
62
- return timestamp > Date.now();
63
- });
64
-
65
- operationsRegistered = true;
66
- }
67
-
68
- /**
69
- * Evaluate a condition against the given context.
70
- *
71
- * A condition may be stated as a literal instead of a rule — `hidden: true`
72
- * rather than `hidden: { "==": [1, 1] }` — and a literal is already its own
73
- * answer, so it is returned rather than handed to the evaluator.
74
- */
75
- export function evaluateCondition(rule: ConditionRule, context: ConditionContext): unknown {
76
- if (typeof rule === "boolean") return rule;
77
- // Ensure operations are registered
78
- registerConditionOperations();
79
- return jsonLogic.apply(rule, context);
80
- }
81
-
82
- /**
83
- * Convert a value to a format suitable for JSON Logic evaluation.
84
- * Specifically handles Date objects by converting them to Unix timestamps.
85
- */
86
- function serializeValueForConditions(value: unknown): unknown {
87
- if (value === null || value === undefined) {
88
- return value;
89
- }
90
-
91
- // Handle Date objects
92
- if (value instanceof Date) {
93
- return value.getTime();
94
- }
95
-
96
- // Handle Firestore Timestamp-like objects (have toDate or toMillis)
97
- if (typeof (value as { toMillis?: () => number })?.toMillis === "function") {
98
- return (value as { toMillis: () => number }).toMillis();
99
- }
100
- if (typeof (value as { toDate?: () => Date })?.toDate === "function") {
101
- return (value as { toDate: () => Date }).toDate().getTime();
102
- }
103
-
104
- // Handle arrays recursively
105
- if (Array.isArray(value)) {
106
- return value.map(serializeValueForConditions);
107
- }
108
-
109
- // Handle plain objects recursively
110
- if (typeof value === "object") {
111
- const result: Record<string, unknown> = {};
112
- for (const key of Object.keys(value as Record<string, unknown>)) {
113
- result[key] = serializeValueForConditions((value as Record<string, unknown>)[key]);
114
- }
115
- return result;
116
- }
117
-
118
- return value;
119
- }
120
-
121
- /**
122
- * Build a ConditionContext from the current property resolution context.
123
- */
124
- export function buildConditionContext(params: {
125
- propertyKey?: string;
126
- values?: Record<string, unknown>;
127
- previousValues?: Record<string, unknown>;
128
- path: string;
129
- entityId?: string;
130
- index?: number;
131
- authController: AuthState;
132
- }): ConditionContext {
133
- const {
134
- propertyKey,
135
- values,
136
- previousValues,
137
- path,
138
- entityId,
139
- index,
140
- authController
141
- } = params;
142
-
143
- const user = authController.user;
144
- const serializedValues = serializeValueForConditions(values ?? {});
145
- const serializedPreviousValues = serializeValueForConditions(previousValues ?? values ?? {});
146
-
147
- return {
148
- values: serializedValues as Record<string, unknown>,
149
- previousValues: serializedPreviousValues as Record<string, unknown>,
150
- propertyValue: propertyKey ? getIn(serializedValues, propertyKey) : undefined,
151
- path,
152
- entityId,
153
- isNew: !entityId,
154
- index,
155
- user: {
156
- uid: user?.uid ?? "",
157
- email: user?.email ?? null,
158
- displayName: user?.displayName ?? null,
159
- photoURL: user?.photoURL ?? null,
160
- roles: (user?.roles ?? []).map((r: unknown) => typeof r === "string" ? r : (r as { id: string }).id)
161
- },
162
- now: Date.now()
163
- };
164
- }
165
-
166
- /**
167
- * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
168
- */
package/src/util/email.ts DELETED
@@ -1,32 +0,0 @@
1
- /**
2
- * Email normalization — one implementation, because the database enforces it.
3
- *
4
- * `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the
5
- * auth table. That index decides what "the same address" means, and it does not
6
- * trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and
7
- * both may exist. So every write that reaches the column has to agree with
8
- * every read, exactly, or the two disagree in the one direction that matters —
9
- * a row that exists and cannot be found.
10
- *
11
- * That is not hypothetical. The lookup path trimmed and the admin create paths
12
- * did not, so a user created through `POST /api/data/users` or
13
- * `POST /api/auth/admin/users` with a stray space was stored untrimmed,
14
- * survived the unique index alongside the real address, and was unreachable by
15
- * login forever after. The HTTP auth routes were unaffected only because Zod's
16
- * `.email()` happens to reject surrounding whitespace — a guard on a different
17
- * layer, for a different reason, that the admin paths do not sit behind.
18
- *
19
- * It lives in `common` because `server`, `server-postgres` and `server-mongo`
20
- * all write this column and must agree exactly, and `common` is the only
21
- * package all three already depend on.
22
- */
23
-
24
- /**
25
- * Canonical form of an email address: trimmed, lower-cased.
26
- *
27
- * Non-strings pass through untouched, so this is safe to apply to a value out
28
- * of a partial update payload whose type is not known yet.
29
- */
30
- export function normalizeEmail<T>(email: T): T | string {
31
- return typeof email === "string" ? email.trim().toLowerCase() : email;
32
- }