@rebasepro/common 0.4.0 → 0.6.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.
@@ -29,9 +29,13 @@ export declare function getRelationFrom<M extends Record<string, unknown>>(entit
29
29
  * Handles EntityRelation class instances, and plain objects
30
30
  * with `__type === "relation"` or an `isEntityRelation()` method.
31
31
  *
32
+ * When `propertyType` is `"relation"`, also accepts plain objects that
33
+ * have `id` and `path` fields — these are relation-shaped objects from
34
+ * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
35
+ *
32
36
  * Returns null if the value cannot be coerced.
33
37
  */
34
- export declare function normalizeToEntityRelation(value: unknown): EntityRelation | null;
38
+ export declare function normalizeToEntityRelation(value: unknown, propertyType?: string): EntityRelation | null;
35
39
  export declare function traverseValuesProperties<M extends Record<string, unknown>>(inputValues: Partial<EntityValues<M>>, properties: Properties, operation: (value: unknown, property: Property) => unknown): EntityValues<M> | undefined;
36
40
  export declare function traverseValueProperty(inputValue: unknown, property: Property, operation: (value: unknown, property: Property) => unknown): unknown;
37
41
  /**
@@ -1,6 +1,14 @@
1
- import { AuthController, Entity, EntityCollection, User } from "@rebasepro/types";
2
- export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, entity: Entity<M> | null, targetOperation: "select" | "insert" | "update" | "delete"): boolean;
3
- export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>): boolean;
4
- export declare function canEditEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
5
- export declare function canCreateEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
6
- export declare function canDeleteEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
1
+ import { Entity, EntityCollection, User } from "@rebasepro/types";
2
+ /**
3
+ * Minimal auth context for permission checking.
4
+ * Only requires the user object avoids forcing callers to construct
5
+ * a full AuthController just to check permissions.
6
+ */
7
+ export interface AuthContext<USER extends User = User> {
8
+ user: USER | null;
9
+ }
10
+ export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, entity: Entity<M> | null, targetOperation: "select" | "insert" | "update" | "delete"): boolean;
11
+ export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>): boolean;
12
+ export declare function canEditEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
13
+ export declare function canCreateEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
14
+ export declare function canDeleteEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/common",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.6.0",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -41,27 +41,27 @@
41
41
  "dependencies": {
42
42
  "fast-equals": "6.0.0",
43
43
  "json-logic-js": "^2.0.5",
44
- "@rebasepro/types": "0.4.0",
45
- "@rebasepro/utils": "0.4.0"
44
+ "@rebasepro/types": "0.6.0",
45
+ "@rebasepro/utils": "0.6.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@jest/globals": "^29.7.0",
48
+ "@jest/globals": "^30.4.1",
49
49
  "@testing-library/react": "^16.3.2",
50
50
  "@testing-library/user-event": "^14.6.1",
51
- "@types/jest": "^29.5.14",
52
- "@types/node": "^20.19.41",
51
+ "@types/jest": "^30.0.0",
52
+ "@types/node": "^25.9.3",
53
53
  "@types/object-hash": "^3.0.6",
54
- "@types/react": "^19.2.15",
54
+ "@types/react": "^19.2.17",
55
55
  "@types/react-dom": "^19.2.3",
56
56
  "@types/react-measure": "^2.0.12",
57
- "@vitejs/plugin-react": "^4.7.0",
57
+ "@vitejs/plugin-react": "^6.0.2",
58
58
  "babel-plugin-react-compiler": "beta",
59
- "cross-env": "^7.0.3",
60
- "jest": "^29.7.0",
61
- "ts-jest": "^29.4.10",
62
- "tsd": "^0.31.2",
63
- "typescript": "^5.9.3",
64
- "vite": "^5.4.21"
59
+ "cross-env": "^10.1.0",
60
+ "jest": "^30.4.2",
61
+ "ts-jest": "^29.4.11",
62
+ "tsd": "^0.33.0",
63
+ "typescript": "^6.0.3",
64
+ "vite": "^8.0.16"
65
65
  },
66
66
  "files": [
67
67
  "dist",
@@ -258,7 +258,7 @@ export class CollectionRegistry {
258
258
  joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
259
259
  onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
260
260
  onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
261
- overrides: relProp.overrides ?? relProp.relation?.overrides,
261
+ overrides: relProp.overrides ?? relProp.relation?.overrides
262
262
  });
263
263
  }
264
264
  } else if (property.type === "map" && property.properties) {
@@ -305,7 +305,7 @@ export class CollectionRegistry {
305
305
  const relation = relations.find(r => r.relationName === name);
306
306
  if (relation) {
307
307
  // we attach the resolved relation to the property
308
- (relationProperty as RelationProperty & { relation?: Relation }).relation = relation;
308
+ relationProperty.relation = relation;
309
309
  } else {
310
310
  console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
311
311
  }
@@ -11,6 +11,7 @@ export const defaultUsersCollection: PostgresCollection = {
11
11
  name: "Users",
12
12
  singularName: "User",
13
13
  slug: "users",
14
+ auth: true,
14
15
  table: "users",
15
16
  schema: "rebase",
16
17
  icon: "Users",
@@ -18,8 +19,10 @@ export const defaultUsersCollection: PostgresCollection = {
18
19
  openEntityMode: "dialog",
19
20
  disableDefaultActions: ["copy"],
20
21
  securityRules: [
21
- { operation: "select", roles: ["admin"] },
22
- { operations: ["insert", "update", "delete"], roles: ["admin"] }
22
+ { operation: "select",
23
+ roles: ["admin"] },
24
+ { operations: ["insert", "update", "delete"],
25
+ roles: ["admin"] }
23
26
  ],
24
27
  sort: ["createdAt", "desc"],
25
28
  properties: {
@@ -32,7 +35,8 @@ export const defaultUsersCollection: PostgresCollection = {
32
35
  email: {
33
36
  name: "Email",
34
37
  type: "string",
35
- validation: { required: true, unique: true }
38
+ validation: { required: true,
39
+ unique: true }
36
40
  },
37
41
  displayName: {
38
42
  name: "Name",
@@ -64,32 +68,39 @@ export const defaultUsersCollection: PostgresCollection = {
64
68
  name: "Password Hash",
65
69
  type: "string",
66
70
  columnName: "password_hash",
67
- ui: { hideFromCollection: true, disabled: { hidden: true } }
71
+ ui: { hideFromCollection: true,
72
+ disabled: { hidden: true } }
68
73
  },
69
74
  emailVerified: {
70
75
  name: "Email Verified",
71
76
  type: "boolean",
72
77
  columnName: "email_verified",
73
78
  defaultValue: false,
74
- ui: { hideFromCollection: true, disabled: { hidden: true } }
79
+ ui: { hideFromCollection: true,
80
+ disabled: { hidden: true } }
75
81
  },
76
82
  emailVerificationToken: {
77
83
  name: "Email Verification Token",
78
84
  type: "string",
79
85
  columnName: "email_verification_token",
80
- ui: { hideFromCollection: true, disabled: { hidden: true } }
86
+ ui: { hideFromCollection: true,
87
+ disabled: { hidden: true } }
81
88
  },
82
89
  emailVerificationSentAt: {
83
90
  name: "Email Verification Sent At",
84
91
  type: "date",
85
92
  columnName: "email_verification_sent_at",
86
- ui: { hideFromCollection: true, disabled: { hidden: true } }
93
+ ui: { hideFromCollection: true,
94
+ disabled: { hidden: true } }
87
95
  },
88
96
  metadata: {
89
97
  name: "Metadata",
90
98
  type: "map",
99
+ keyValue: true,
100
+ properties: {},
91
101
  defaultValue: {},
92
- ui: { hideFromCollection: true, disabled: { hidden: true } }
102
+ ui: { hideFromCollection: true,
103
+ disabled: { hidden: true } }
93
104
  },
94
105
  createdAt: {
95
106
  name: "Created At",
@@ -103,7 +114,8 @@ export const defaultUsersCollection: PostgresCollection = {
103
114
  type: "date",
104
115
  columnName: "updated_at",
105
116
  autoValue: "on_update",
106
- ui: { hideFromCollection: true, disabled: { hidden: true } }
117
+ ui: { hideFromCollection: true,
118
+ disabled: { hidden: true } }
107
119
  }
108
120
  },
109
121
  listProperties: ["displayName", "email", "roles", "createdAt"],
@@ -1,15 +1,19 @@
1
1
  import { FindParams, Entity, FindResponse, CollectionAccessor, QueryBuilderInterface, FilterOperator, LogicalCondition, WhereValue, FilterCondition } from "@rebasepro/types";
2
2
 
3
3
  export function or(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
4
- return { type: "or", conditions };
4
+ return { type: "or",
5
+ conditions };
5
6
  }
6
7
 
7
8
  export function and(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
8
- return { type: "and", conditions };
9
+ return { type: "and",
10
+ conditions };
9
11
  }
10
12
 
11
13
  export function cond(column: string, operator: FilterOperator, value: unknown): FilterCondition {
12
- return { column, operator, value };
14
+ return { column,
15
+ operator,
16
+ value };
13
17
  }
14
18
 
15
19
  export class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {
@@ -156,9 +156,13 @@ export function getRelationFrom<M extends Record<string, unknown>>(entity: Entit
156
156
  * Handles EntityRelation class instances, and plain objects
157
157
  * with `__type === "relation"` or an `isEntityRelation()` method.
158
158
  *
159
+ * When `propertyType` is `"relation"`, also accepts plain objects that
160
+ * have `id` and `path` fields — these are relation-shaped objects from
161
+ * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
162
+ *
159
163
  * Returns null if the value cannot be coerced.
160
164
  */
161
- export function normalizeToEntityRelation(value: unknown): EntityRelation | null {
165
+ export function normalizeToEntityRelation(value: unknown, propertyType?: string): EntityRelation | null {
162
166
  if (value instanceof EntityRelation) return value;
163
167
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
164
168
 
@@ -167,7 +171,8 @@ export function normalizeToEntityRelation(value: unknown): EntityRelation | null
167
171
  obj.__type === "relation" ||
168
172
  obj.__type === "reference" ||
169
173
  (typeof obj.isEntityRelation === "function" && (obj.isEntityRelation as () => boolean)()) ||
170
- (typeof obj.isEntityReference === "function" && (obj.isEntityReference as () => boolean)());
174
+ (typeof obj.isEntityReference === "function" && (obj.isEntityReference as () => boolean)()) ||
175
+ (propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string");
171
176
 
172
177
  if (!isRelationLike) return null;
173
178
 
@@ -263,7 +268,9 @@ export interface RelationRefWithData extends RelationRef {
263
268
  * Replaces inline `{ id, path, __type: "relation" }` object literals.
264
269
  */
265
270
  export function createRelationRef(id: string | number, path: string): RelationRef {
266
- return { id, path, __type: "relation" };
271
+ return { id,
272
+ path,
273
+ __type: "relation" };
267
274
  }
268
275
 
269
276
  /**
@@ -271,5 +278,8 @@ export function createRelationRef(id: string | number, path: string): RelationRe
271
278
  * Used when entity data has been pre-fetched (e.g., via batch loading or JOINs).
272
279
  */
273
280
  export function createRelationRefWithData(id: string | number, path: string, data: Entity): RelationRefWithData {
274
- return { id, path, __type: "relation", data };
281
+ return { id,
282
+ path,
283
+ __type: "relation",
284
+ data };
275
285
  }
@@ -1,6 +1,15 @@
1
- import { AuthController, CollectionWithRelations, Entity, EntityCollection, getDataSourceCapabilities, SecurityRule, User } from "@rebasepro/types";
1
+ import { CollectionWithRelations, Entity, EntityCollection, getDataSourceCapabilities, SecurityRule, User } from "@rebasepro/types";
2
+
3
+ /**
4
+ * Minimal auth context for permission checking.
5
+ * Only requires the user object — avoids forcing callers to construct
6
+ * a full AuthController just to check permissions.
7
+ */
8
+ export interface AuthContext<USER extends User = User> {
9
+ user: USER | null;
10
+ }
2
11
 
3
- function evaluateAST<USER extends User, M extends Record<string, unknown>>(sqlString: string, auth: AuthController<USER>, entity: Entity<M> | null): boolean {
12
+ function evaluateAST<USER extends User, M extends Record<string, unknown>>(sqlString: string, auth: AuthContext<USER>, entity: Entity<M> | null): boolean {
4
13
  // This is a client-side SQL evaluator used *only* for optimistic UI updates.
5
14
  // It parses basic AND / OR statements to evaluate RLS without backend roundtrips.
6
15
  if (!entity) return true;
@@ -111,7 +120,7 @@ function evaluateAST<USER extends User, M extends Record<string, unknown>>(sqlSt
111
120
  return true; // Optimistic fallback for anything else
112
121
  }
113
122
 
114
- function evaluateRule<USER extends User, M extends Record<string, unknown>>(rule: SecurityRule, auth: AuthController<USER>, entity: Entity<M> | null): boolean {
123
+ function evaluateRule<USER extends User, M extends Record<string, unknown>>(rule: SecurityRule, auth: AuthContext<USER>, entity: Entity<M> | null): boolean {
115
124
 
116
125
  if (rule.access === "public") return true;
117
126
 
@@ -137,15 +146,12 @@ function evaluateRule<USER extends User, M extends Record<string, unknown>>(rule
137
146
 
138
147
  export function checkOperation<M extends Record<string, unknown>, USER extends User>(
139
148
  collection: EntityCollection<M>,
140
- authController: AuthController<USER>,
149
+ authContext: AuthContext<USER>,
141
150
  entity: Entity<M> | null,
142
151
  targetOperation: "select" | "insert" | "update" | "delete"
143
152
  ): boolean {
144
153
  const securityRules = getDataSourceCapabilities(collection.driver).supportsRLS ? (collection as CollectionWithRelations).securityRules : undefined;
145
154
  if (!securityRules || securityRules.length === 0) {
146
- // According to our plan: Postgres RLS implicitly denies if enabled without rules.
147
- // But for Rebase we default to true if securityRules is undefined,
148
- // so as not to break everything without rules. Let's assume true for now.
149
155
  return true;
150
156
  }
151
157
 
@@ -158,15 +164,13 @@ export function checkOperation<M extends Record<string, unknown>, USER extends U
158
164
 
159
165
  if (applicableRules.length === 0) return false;
160
166
 
161
- // In Postgres, policies ONLY apply if the user matching the targeted roles.
162
- const userRoleIds = authController.user?.roles ?? [];
167
+ const userRoleIds = authContext.user?.roles ?? [];
163
168
  const userRoles = [...userRoleIds, "public"];
164
169
  const roleApplicableRules = applicableRules.filter((rule: SecurityRule) => {
165
- if (!rule.roles || rule.roles.length === 0) return true; // APPLIES TO PUBLIC
170
+ if (!rule.roles || rule.roles.length === 0) return true;
166
171
  return rule.roles.some((r: string) => userRoles.includes(r));
167
172
  });
168
173
 
169
- // If no rules apply to this user's roles, the operation is implicitly denied.
170
174
  if (roleApplicableRules.length === 0) return false;
171
175
 
172
176
  let grantedByPermissive = false;
@@ -174,11 +178,11 @@ export function checkOperation<M extends Record<string, unknown>, USER extends U
174
178
 
175
179
  for (const rule of roleApplicableRules) {
176
180
  const mode = rule.mode || "permissive";
177
- const passed = evaluateRule(rule, authController, entity);
181
+ const passed = evaluateRule(rule, authContext, entity);
178
182
 
179
183
  if (mode === "restrictive" && !passed) {
180
184
  deniedByRestrictive = true;
181
- break; // Immediate deny
185
+ break;
182
186
  }
183
187
 
184
188
  if (mode === "permissive" && passed) {
@@ -199,37 +203,37 @@ export function checkOperation<M extends Record<string, unknown>, USER extends U
199
203
  export function canReadCollection<M extends Record<string, unknown>, USER extends User>
200
204
  (
201
205
  collection: EntityCollection<M>,
202
- authController: AuthController<USER>
206
+ authContext: AuthContext<USER>
203
207
  ): boolean {
204
- return checkOperation(collection, authController, null, "select");
208
+ return checkOperation(collection, authContext, null, "select");
205
209
  }
206
210
 
207
211
  export function canEditEntity<M extends Record<string, unknown>, USER extends User>
208
212
  (
209
213
  collection: EntityCollection<M>,
210
- authController: AuthController<USER>,
214
+ authContext: AuthContext<USER>,
211
215
  path: string,
212
216
  entity: Entity<M> | null
213
217
  ): boolean {
214
- return checkOperation(collection, authController, entity, "update");
218
+ return checkOperation(collection, authContext, entity, "update");
215
219
  }
216
220
 
217
221
  export function canCreateEntity<M extends Record<string, unknown>, USER extends User>
218
222
  (
219
223
  collection: EntityCollection<M>,
220
- authController: AuthController<USER>,
224
+ authContext: AuthContext<USER>,
221
225
  path: string,
222
226
  entity: Entity<M> | null
223
227
  ): boolean {
224
- return checkOperation(collection, authController, entity, "insert");
228
+ return checkOperation(collection, authContext, entity, "insert");
225
229
  }
226
230
 
227
231
  export function canDeleteEntity<M extends Record<string, unknown>, USER extends User>
228
232
  (
229
233
  collection: EntityCollection<M>,
230
- authController: AuthController<USER>,
234
+ authContext: AuthContext<USER>,
231
235
  path: string,
232
236
  entity: Entity<M> | null
233
237
  ): boolean {
234
- return checkOperation(collection, authController, entity, "delete");
238
+ return checkOperation(collection, authContext, entity, "delete");
235
239
  }
@@ -19,7 +19,8 @@ export function sanitizeRelation(
19
19
  targetCollection = resolveCollection(rawTarget);
20
20
  }
21
21
  if (!targetCollection) {
22
- targetCollection = { slug: rawTarget, name: rawTarget } as EntityCollection;
22
+ targetCollection = { slug: rawTarget,
23
+ name: rawTarget } as EntityCollection;
23
24
  }
24
25
  } else if (typeof rawTarget === "function") {
25
26
  const evaluated = rawTarget();
@@ -28,7 +29,8 @@ export function sanitizeRelation(
28
29
  targetCollection = resolveCollection(evaluated);
29
30
  }
30
31
  if (!targetCollection) {
31
- targetCollection = { slug: evaluated, name: evaluated } as EntityCollection;
32
+ targetCollection = { slug: evaluated,
33
+ name: evaluated } as EntityCollection;
32
34
  }
33
35
  } else {
34
36
  targetCollection = evaluated;
@@ -295,7 +297,7 @@ export function resolvePropertyRelation({
295
297
  joinPath: relProp.joinPath,
296
298
  onUpdate: relProp.onUpdate,
297
299
  onDelete: relProp.onDelete,
298
- overrides: relProp.overrides,
300
+ overrides: relProp.overrides
299
301
  } as Relation;
300
302
  }
301
303
 
@@ -354,7 +354,7 @@ export function getSubcollections<M extends Record<string, unknown> = Record<str
354
354
  if (getDataSourceCapabilities(collection.driver).supportsRelations) {
355
355
  const resolvedRelations = resolveCollectionRelations(collection);
356
356
  const manyRelations = Object.values(resolvedRelations).filter((r: Relation) => r.cardinality === "many");
357
-
357
+
358
358
  return manyRelations.map((r: Relation) => {
359
359
  const target = r.target();
360
360
  if (!target) return undefined;
@@ -377,7 +377,8 @@ export function getSubcollections<M extends Record<string, unknown> = Record<str
377
377
  baseOverrides.singularName = customName;
378
378
  }
379
379
 
380
- const targetWithOverrides = { ...target, ...baseOverrides };
380
+ const targetWithOverrides = { ...target,
381
+ ...baseOverrides };
381
382
  return (r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides) as EntityCollection<Record<string, unknown>>;
382
383
  }).filter((c: EntityCollection<Record<string, unknown>> | undefined): c is EntityCollection<Record<string, unknown>> => Boolean(c));
383
384
  }