@rebasepro/common 0.3.0 → 0.5.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.
@@ -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.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -41,8 +41,8 @@
41
41
  "dependencies": {
42
42
  "fast-equals": "6.0.0",
43
43
  "json-logic-js": "^2.0.5",
44
- "@rebasepro/types": "0.3.0",
45
- "@rebasepro/utils": "0.3.0"
44
+ "@rebasepro/types": "0.5.0",
45
+ "@rebasepro/utils": "0.5.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@jest/globals": "^29.7.0",
@@ -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
  }
@@ -1,14 +1,11 @@
1
- import { PostgresCollection } from "@rebasepro/types";
1
+ import type { PostgresCollection } from "@rebasepro/types";
2
2
 
3
3
  /**
4
- * Default users collection definition.
4
+ * Default users collection.
5
5
  *
6
- * Shared between the admin UI (for navigation/display) and the backend
7
- * (for schema generation). Both consumers prepend this to the developer's
8
- * collections array and rely on generic slug-based deduplication
9
- * (Map keyed by slug, last-write-wins) so that developer-defined
10
- * collections with the same slug override this default — no hardcoded
11
- * string checks required.
6
+ * Prepended to the developer's collections array by the admin and server.
7
+ * Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
8
+ * override by defining their own collection with `slug: "users"`.
12
9
  */
13
10
  export const defaultUsersCollection: PostgresCollection = {
14
11
  name: "Users",
@@ -18,62 +15,97 @@ export const defaultUsersCollection: PostgresCollection = {
18
15
  schema: "rebase",
19
16
  icon: "Users",
20
17
  group: "Settings",
18
+ openEntityMode: "dialog",
19
+ disableDefaultActions: ["copy"],
20
+ securityRules: [
21
+ { operation: "select", roles: ["admin"] },
22
+ { operations: ["insert", "update", "delete"], roles: ["admin"] }
23
+ ],
24
+ sort: ["createdAt", "desc"],
21
25
  properties: {
22
26
  id: {
23
27
  name: "ID",
24
28
  type: "string",
25
- isId: "uuid"
29
+ isId: "uuid",
30
+ ui: { readOnly: true }
26
31
  },
27
32
  email: {
28
33
  name: "Email",
29
34
  type: "string",
30
35
  validation: { required: true, unique: true }
31
36
  },
32
- password_hash: {
33
- name: "Password Hash",
37
+ displayName: {
38
+ name: "Name",
34
39
  type: "string",
35
- ui: { hideFromCollection: true }
36
- },
37
- display_name: {
38
- name: "Display Name",
39
- type: "string"
40
+ columnName: "display_name",
41
+ validation: { required: true }
40
42
  },
41
- photo_url: {
43
+ photoURL: {
42
44
  name: "Photo URL",
43
- type: "string"
45
+ type: "string",
46
+ columnName: "photo_url",
47
+ url: "image"
48
+ },
49
+ roles: {
50
+ name: "Roles",
51
+ type: "array",
52
+ columnType: "text[]",
53
+ of: {
54
+ name: "Role",
55
+ type: "string",
56
+ enum: {
57
+ admin: "Admin",
58
+ editor: "Editor",
59
+ viewer: "Viewer"
60
+ }
61
+ }
62
+ },
63
+ passwordHash: {
64
+ name: "Password Hash",
65
+ type: "string",
66
+ columnName: "password_hash",
67
+ ui: { hideFromCollection: true, disabled: { hidden: true } }
44
68
  },
45
- email_verified: {
69
+ emailVerified: {
46
70
  name: "Email Verified",
47
71
  type: "boolean",
48
- defaultValue: false
72
+ columnName: "email_verified",
73
+ defaultValue: false,
74
+ ui: { hideFromCollection: true, disabled: { hidden: true } }
49
75
  },
50
- email_verification_token: {
76
+ emailVerificationToken: {
51
77
  name: "Email Verification Token",
52
78
  type: "string",
53
- ui: { hideFromCollection: true }
79
+ columnName: "email_verification_token",
80
+ ui: { hideFromCollection: true, disabled: { hidden: true } }
54
81
  },
55
- email_verification_sent_at: {
82
+ emailVerificationSentAt: {
56
83
  name: "Email Verification Sent At",
57
84
  type: "date",
58
- ui: { hideFromCollection: true }
85
+ columnName: "email_verification_sent_at",
86
+ ui: { hideFromCollection: true, disabled: { hidden: true } }
59
87
  },
60
88
  metadata: {
61
89
  name: "Metadata",
62
90
  type: "map",
63
91
  defaultValue: {},
64
- ui: { hideFromCollection: true }
92
+ ui: { hideFromCollection: true, disabled: { hidden: true } }
65
93
  },
66
- created_at: {
94
+ createdAt: {
67
95
  name: "Created At",
68
96
  type: "date",
97
+ columnName: "created_at",
69
98
  autoValue: "on_create",
70
- ui: { readOnly: true, hideFromCollection: true }
99
+ ui: { readOnly: true }
71
100
  },
72
- updated_at: {
101
+ updatedAt: {
73
102
  name: "Updated At",
74
103
  type: "date",
104
+ columnName: "updated_at",
75
105
  autoValue: "on_update",
76
- ui: { readOnly: true, hideFromCollection: true }
106
+ ui: { hideFromCollection: true, disabled: { hidden: true } }
77
107
  }
78
- }
108
+ },
109
+ listProperties: ["displayName", "email", "roles", "createdAt"],
110
+ propertiesOrder: ["id", "email", "displayName", "roles", "createdAt"]
79
111
  };
@@ -8,7 +8,10 @@ import {
8
8
  EntityValues,
9
9
  FilterValues,
10
10
  WhereFilterOp,
11
- WhereFieldValue
11
+ WhereFieldValue,
12
+ WhereFilterOpShort,
13
+ LogicalCondition,
14
+ WhereValue
12
15
  } from "@rebasepro/types";
13
16
  import { toSnakeCase } from "@rebasepro/utils";
14
17
  import { QueryBuilder } from "./query_builder";
@@ -69,11 +72,18 @@ function convertWhereToFilter(where?: Record<string, WhereFieldValue>): FilterVa
69
72
  continue;
70
73
  }
71
74
 
72
- // Handle tuple: [operator, value]
73
- if (Array.isArray(rawValue) && rawValue.length === 2) {
74
- const [rawOp, val] = rawValue;
75
- const mappedOp = operatorMap[rawOp] ?? "==";
76
- filter[field] = [mappedOp, val];
75
+ // Handle tuple or array of tuples
76
+ if (Array.isArray(rawValue)) {
77
+ const conditions: [WhereFilterOpShort, unknown][] = Array.isArray(rawValue[0])
78
+ ? (rawValue as [WhereFilterOpShort, unknown][])
79
+ : [rawValue as [WhereFilterOpShort, unknown]];
80
+
81
+ const mappedConditions: [WhereFilterOp, unknown][] = conditions.map(([rawOp, val]) => {
82
+ const mappedOp = operatorMap[rawOp] ?? "==";
83
+ return [mappedOp, val];
84
+ });
85
+
86
+ filter[field] = Array.isArray(rawValue[0]) ? mappedConditions : mappedConditions[0];
77
87
  continue;
78
88
  }
79
89
 
@@ -190,6 +200,12 @@ values: {} as Record<string, unknown> }
190
200
  });
191
201
  },
192
202
 
203
+ deleteAll: driver.deleteAll
204
+ ? async (): Promise<void> => {
205
+ return driver.deleteAll!(slug);
206
+ }
207
+ : undefined,
208
+
193
209
  count: driver.countEntities
194
210
  ? async (params?: FindParams): Promise<number> => {
195
211
  return driver.countEntities!({
@@ -238,8 +254,12 @@ values: {} as Record<string, unknown> }
238
254
  } : undefined,
239
255
 
240
256
  // Fluent Query Builder
241
- where(column: keyof M & string, operator: WhereFilterOp, value: unknown) {
242
- return new QueryBuilder<M>(accessor).where(column, operator, value);
257
+ where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOpShort, value?: unknown) {
258
+ const builder = new QueryBuilder<M>(accessor);
259
+ if (typeof columnOrCondition === "object") {
260
+ return builder.where(columnOrCondition);
261
+ }
262
+ return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
243
263
  },
244
264
  orderBy(column: keyof M & string, ascending?: "asc" | "desc") {
245
265
  return new QueryBuilder<M>(accessor).orderBy(column, ascending);
@@ -1,21 +1,15 @@
1
- import { FindParams, Entity, FindResponse, CollectionAccessor, QueryBuilderInterface, FilterOperator } from "@rebasepro/types";
2
-
3
- /**
4
- * Maps standard operators to Rebase backend's string-based operators
5
- */
6
- function mapOperator(op: FilterOperator): string {
7
- switch (op) {
8
- case "==": return "eq";
9
- case "!=": return "neq";
10
- case ">": return "gt";
11
- case ">=": return "gte";
12
- case "<": return "lt";
13
- case "<=": return "lte";
14
- case "array-contains": return "cs";
15
- case "array-contains-any": return "csa";
16
- case "not-in": return "nin";
17
- default: return op;
18
- }
1
+ import { FindParams, Entity, FindResponse, CollectionAccessor, QueryBuilderInterface, FilterOperator, LogicalCondition, WhereValue, FilterCondition } from "@rebasepro/types";
2
+
3
+ export function or(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
4
+ return { type: "or", conditions };
5
+ }
6
+
7
+ export function and(...conditions: (FilterCondition | LogicalCondition)[]): LogicalCondition {
8
+ return { type: "and", conditions };
9
+ }
10
+
11
+ export function cond(column: string, operator: FilterOperator, value: unknown): FilterCondition {
12
+ return { column, operator, value };
19
13
  }
20
14
 
21
15
  export class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {
@@ -28,22 +22,38 @@ export class QueryBuilder<M extends Record<string, unknown> = Record<string, unk
28
22
  * @example
29
23
  * client.collection('users').where('age', '>=', 18).find()
30
24
  */
31
- where(column: keyof M & string, operator: FilterOperator, value: unknown): this {
25
+ where<K extends keyof M & string>(column: K, operator: FilterOperator, value: WhereValue<M[K]>): this;
26
+ where(logicalCondition: LogicalCondition): this;
27
+ where(columnOrCondition: string | LogicalCondition, operator?: FilterOperator, value?: unknown): this {
28
+ // Handle LogicalCondition signature
29
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
30
+ this.params.logical = columnOrCondition as LogicalCondition;
31
+ return this;
32
+ }
33
+
32
34
  if (!this.params.where) {
33
35
  this.params.where = {};
34
36
  }
35
37
 
36
- const mappedOp = mapOperator(operator);
37
- let formattedValue = value;
38
+ const column = columnOrCondition as string;
39
+ const condition: [FilterOperator, unknown] = [operator!, value];
40
+ const existing = this.params.where[column];
38
41
 
39
- // Handle arrays for in, nin, cs, csa
40
- if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
41
- formattedValue = `(${value.join(",")})`;
42
- } else if (value === null) {
43
- formattedValue = "null";
42
+ if (existing === undefined) {
43
+ this.params.where[column] = condition;
44
+ } else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {
45
+ (this.params.where[column] as [FilterOperator, unknown][]).push(condition);
46
+ } else {
47
+ // Convert existing single tuple/value into array of tuples
48
+ let firstCondition: [FilterOperator, unknown];
49
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") {
50
+ firstCondition = existing as [FilterOperator, unknown];
51
+ } else {
52
+ firstCondition = ["==", existing];
53
+ }
54
+ this.params.where[column] = [firstCondition, condition];
44
55
  }
45
56
 
46
- this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
47
57
  return this;
48
58
  }
49
59
 
@@ -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
  }