@rebasepro/server-postgresql 0.2.4 → 0.2.5

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 (37) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +5 -8
  2. package/dist/index.es.js +286 -365
  3. package/dist/index.es.js.map +1 -1
  4. package/dist/index.umd.js +283 -362
  5. package/dist/index.umd.js.map +1 -1
  6. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
  7. package/dist/server-postgresql/src/auth/services.d.ts +6 -31
  8. package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
  9. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  10. package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
  11. package/dist/types/src/controllers/auth.d.ts +2 -2
  12. package/dist/types/src/controllers/client.d.ts +25 -40
  13. package/dist/types/src/controllers/data.d.ts +4 -0
  14. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  15. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  16. package/dist/types/src/types/backend.d.ts +2 -2
  17. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  18. package/dist/types/src/types/properties.d.ts +7 -5
  19. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  20. package/dist/types/src/users/index.d.ts +0 -1
  21. package/dist/types/src/users/user.d.ts +0 -1
  22. package/package.json +6 -6
  23. package/src/PostgresBackendDriver.ts +10 -0
  24. package/src/PostgresBootstrapper.ts +3 -12
  25. package/src/auth/ensure-tables.ts +52 -101
  26. package/src/auth/services.ts +71 -170
  27. package/src/schema/auth-schema.ts +13 -69
  28. package/src/schema/doctor.ts +44 -3
  29. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  30. package/src/schema/introspect-db-logic.ts +7 -0
  31. package/src/services/EntityPersistService.ts +9 -0
  32. package/src/services/entityService.ts +7 -0
  33. package/test/auth-services.test.ts +7 -150
  34. package/test/doctor.test.ts +6 -2
  35. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  36. package/dist/types/src/users/roles.d.ts +0 -14
  37. package/src/schema/default-collections.ts +0 -69
@@ -5,29 +5,7 @@ import { getColumnMeta } from "../services/entity-helpers";
5
5
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
6
  import { logger } from "@rebasepro/server-core";
7
7
 
8
- /**
9
- * Default roles to seed on first run
10
- */
11
- const DEFAULT_ROLES = [
12
- {
13
- id: "admin",
14
- name: "Admin",
15
- is_admin: true,
16
- default_permissions: { read: true, create: true, edit: true, delete: true }
17
- },
18
- {
19
- id: "editor",
20
- name: "Editor",
21
- is_admin: false,
22
- default_permissions: { read: true, create: true, edit: true, delete: true }
23
- },
24
- {
25
- id: "viewer",
26
- name: "Viewer",
27
- is_admin: false,
28
- default_permissions: { read: true, create: false, edit: false, delete: false }
29
- }
30
- ];
8
+
31
9
 
32
10
  /**
33
11
  * Auto-create auth tables if they don't exist
@@ -64,31 +42,19 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
64
42
  }
65
43
  }
66
44
 
67
- // Resolve dynamic roles schema name
68
- let rolesSchema = "rebase";
69
- if (registry) {
70
- const rolesTable = registry.getTable("roles");
71
- if (rolesTable) {
72
- rolesSchema = getTableConfig(rolesTable).schema || "public";
73
- }
74
- }
45
+
75
46
 
76
47
  // ── Create schemas (idempotent) ──────────────────────────────────
77
48
  if (usersSchema !== "public") {
78
49
  await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);
79
50
  }
80
- if (rolesSchema !== "public" && rolesSchema !== usersSchema) {
81
- await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
82
- }
83
51
  await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
84
52
 
85
- // Dynamic table names
86
- const userIdentitiesTable = `"${rolesSchema}"."user_identities"`;
87
- const rolesTableName = `"${rolesSchema}"."roles"`;
88
- const userRolesTableName = `"${rolesSchema}"."user_roles"`;
89
- const refreshTokensTableName = `"${rolesSchema}"."refresh_tokens"`;
90
- const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
91
- const appConfigTableName = `"${rolesSchema}"."app_config"`;
53
+ const authSchema = usersSchema === "public" ? "rebase" : usersSchema;
54
+ const userIdentitiesTable = `"${authSchema}"."user_identities"`;
55
+ const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
56
+ const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
57
+ const appConfigTableName = `"${authSchema}"."app_config"`;
92
58
 
93
59
  // ── Create tables (idempotent) ──────────────────────────────────
94
60
 
@@ -113,32 +79,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
113
79
  `);
114
80
 
115
81
 
116
- // Create roles table
117
- await db.execute(sql`
118
- CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
119
- id TEXT PRIMARY KEY,
120
- name TEXT NOT NULL,
121
- is_admin BOOLEAN DEFAULT FALSE,
122
- default_permissions JSONB,
123
- collection_permissions JSONB,
124
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
125
- )
126
- `);
127
-
128
- // Create user_roles junction table
129
- await db.execute(sql`
130
- CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
131
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
132
- role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
133
- PRIMARY KEY (user_id, role_id)
134
- )
135
- `);
136
-
137
- // Create index on user_id for faster lookups
138
- await db.execute(sql`
139
- CREATE INDEX IF NOT EXISTS idx_user_roles_user
140
- ON ${sql.raw(userRolesTableName)}(user_id)
141
- `);
142
82
 
143
83
  // Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
144
84
  await db.execute(sql`
@@ -229,7 +169,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
229
169
  });
230
170
 
231
171
  // Seed default roles if none exist
232
- await seedDefaultRoles(db, rolesTableName);
172
+ // (no-op: roles are now stored inline on the users table)
233
173
 
234
174
  // ── Migration: Add is_anonymous column (safe for existing tables) ────
235
175
  await db.execute(sql`
@@ -237,10 +177,51 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
237
177
  ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
238
178
  `);
239
179
 
180
+ // ── Migration: Add inline roles column (safe for existing tables) ────
181
+ await db.execute(sql`
182
+ ALTER TABLE ${sql.raw(usersTableName)}
183
+ ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
184
+ `);
185
+
186
+ // ── Migration: Copy roles from legacy junction table to inline column ──
187
+ // If the old rebase.user_roles and rebase.roles tables exist, migrate
188
+ // the data into the new TEXT[] column then drop the legacy tables.
189
+ try {
190
+ const legacyCheck = await db.execute(sql`
191
+ SELECT EXISTS (
192
+ SELECT 1 FROM information_schema.tables
193
+ WHERE table_schema = 'rebase' AND table_name = 'user_roles'
194
+ ) AS has_user_roles
195
+ `);
196
+ const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;
197
+
198
+ if (hasLegacyTables) {
199
+ logger.info("🔄 Migrating roles from legacy user_roles table...");
200
+ // Update users' roles column from the junction table
201
+ await db.execute(sql`
202
+ UPDATE ${sql.raw(usersTableName)} u
203
+ SET roles = COALESCE((
204
+ SELECT array_agg(ur.role_id)
205
+ FROM "rebase"."user_roles" ur
206
+ WHERE ur.user_id = u.id
207
+ ), '{}')
208
+ WHERE u.roles = '{}' OR u.roles IS NULL
209
+ `);
210
+
211
+ // Drop legacy tables (junction first due to FK)
212
+ await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
213
+ await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
214
+ logger.info("✅ Legacy roles tables migrated and dropped");
215
+ }
216
+ } catch (migrationError: unknown) {
217
+ // Non-fatal: log and continue — the column exists and will work
218
+ logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
219
+ }
220
+
240
221
  // ── MFA tables ──────────────────────────────────────────────────────
241
- const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
242
- const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
243
- const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
222
+ const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
223
+ const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
224
+ const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
244
225
 
245
226
  // Create mfa_factors table
246
227
  await db.execute(sql`
@@ -304,33 +285,3 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
304
285
  }
305
286
  }
306
287
 
307
- /**
308
- * Seed default roles if the roles table is empty
309
- */
310
- async function seedDefaultRoles(db: NodePgDatabase, rolesTableName: string): Promise<void> {
311
- // Check if any roles exist
312
- const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
313
- const count = parseInt((result.rows[0] as Record<string, string | number>)?.count as string || "0", 10);
314
-
315
- if (count > 0) {
316
- logger.info(`📋 Found ${count} existing roles`);
317
- return;
318
- }
319
-
320
- logger.info("🌱 Seeding default roles...");
321
-
322
- for (const role of DEFAULT_ROLES) {
323
- await db.execute(sql`
324
- INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
325
- VALUES (
326
- ${role.id},
327
- ${role.name},
328
- ${role.is_admin},
329
- ${JSON.stringify(role.default_permissions)}::jsonb
330
- )
331
- ON CONFLICT (id) DO NOTHING
332
- `);
333
- }
334
-
335
- logger.info("✅ Default roles created: admin, editor, viewer");
336
- }
@@ -1,7 +1,7 @@
1
1
  import { eq, getTableName, sql } from "drizzle-orm";
2
2
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import { getTableConfig, PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
4
- import { users, roles, userRoles, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
4
+ import { users, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
5
5
  import {
6
6
  UserRepository,
7
7
  RoleRepository,
@@ -28,8 +28,6 @@ export type { Role };
28
28
 
29
29
  export interface AuthSchemaTables {
30
30
  users: PgTable & Record<string, AnyPgColumn>;
31
- roles: PgTable & Record<string, AnyPgColumn>;
32
- userRoles: PgTable & Record<string, AnyPgColumn>;
33
31
  refreshTokens: PgTable & Record<string, AnyPgColumn>;
34
32
  passwordResetTokens: PgTable & Record<string, AnyPgColumn>;
35
33
  appConfig: PgTable & Record<string, AnyPgColumn>;
@@ -61,25 +59,19 @@ function getColumn(table: (PgTable & Record<string, AnyPgColumn>) | undefined, .
61
59
  export class UserService implements UserRepository {
62
60
  private usersTable: PgTable & Record<string, AnyPgColumn>;
63
61
  private userIdentitiesTable: PgTable & Record<string, AnyPgColumn>;
64
- private userRolesTable: PgTable & Record<string, AnyPgColumn>;
65
- private rolesTable: PgTable & Record<string, AnyPgColumn>;
66
62
 
67
63
  constructor(
68
64
  private db: NodePgDatabase,
69
65
  tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
70
66
  ) {
71
- if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users || (tableOrTables as Partial<AuthSchemaTables>).roles)) {
67
+ if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users)) {
72
68
  const tables = tableOrTables as Partial<AuthSchemaTables>;
73
69
  this.usersTable = (tables.users || users) as unknown as PgTable & Record<string, AnyPgColumn>;
74
70
  this.userIdentitiesTable = (tables.userIdentities || userIdentities) as unknown as PgTable & Record<string, AnyPgColumn>;
75
- this.userRolesTable = (tables.userRoles || userRoles) as unknown as PgTable & Record<string, AnyPgColumn>;
76
- this.rolesTable = (tables.roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
77
71
  } else {
78
72
  const table = tableOrTables as (PgTable & Record<string, AnyPgColumn>) | undefined;
79
73
  this.usersTable = table || (users as unknown as PgTable & Record<string, AnyPgColumn>);
80
74
  this.userIdentitiesTable = userIdentities as unknown as PgTable & Record<string, AnyPgColumn>;
81
- this.userRolesTable = userRoles as unknown as PgTable & Record<string, AnyPgColumn>;
82
- this.rolesTable = roles as unknown as PgTable & Record<string, AnyPgColumn>;
83
75
  }
84
76
  }
85
77
 
@@ -115,6 +107,7 @@ export class UserService implements UserRepository {
115
107
  "email_verification_token", "emailVerificationToken",
116
108
  "email_verification_sent_at", "emailVerificationSentAt",
117
109
  "is_anonymous", "isAnonymous",
110
+ "roles",
118
111
  "created_at", "createdAt",
119
112
  "updated_at", "updatedAt",
120
113
  "metadata"
@@ -315,11 +308,9 @@ export class UserService implements UserRepository {
315
308
  const idColumn = idCol ? idCol.name : "id";
316
309
 
317
310
  const usersTableName = this.getQualifiedUsersTableName();
318
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
319
-
320
311
  const conditions = [];
321
312
  if (roleId) {
322
- conditions.push(sql`EXISTS (SELECT 1 FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)} AND ur.role_id = ${roleId})`);
313
+ conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);
323
314
  }
324
315
  if (search) {
325
316
  const pattern = `%${search}%`;
@@ -331,7 +322,7 @@ export class UserService implements UserRepository {
331
322
  // Sorting: users with roles first if no role filter, then by requested order
332
323
  const orderByClause = roleId
333
324
  ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`
334
- : sql`ORDER BY (SELECT count(*) FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur WHERE ur.user_id = ${sql.raw(usersTableName)}.${sql.raw(idColumn)}) DESC, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
325
+ : sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
335
326
 
336
327
  const countResult = await this.db.execute(sql`
337
328
  SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
@@ -428,23 +419,25 @@ export class UserService implements UserRepository {
428
419
  }
429
420
 
430
421
  /**
431
- * Get roles for a user from database
422
+ * Get roles for a user from database (inline TEXT[] column)
432
423
  */
433
424
  async getUserRoles(userId: string): Promise<Role[]> {
434
- const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
425
+ const usersTableName = this.getQualifiedUsersTableName();
435
426
  const result = await this.db.execute(sql`
436
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
437
- FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
438
- INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
439
- WHERE ur.user_id = ${userId}
427
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
440
428
  `);
441
429
 
442
- return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null }>).map(row => ({
443
- id: row.id,
444
- name: row.name,
445
- isAdmin: row.is_admin,
446
- defaultPermissions: row.default_permissions,
447
- collectionPermissions: row.collection_permissions
430
+ if (result.rows.length === 0) return [];
431
+
432
+ const row = result.rows[0] as { roles: string[] | null };
433
+ const roleIds = row.roles ?? [];
434
+
435
+ return roleIds.map(id => ({
436
+ id,
437
+ name: id,
438
+ isAdmin: id === "admin",
439
+ defaultPermissions: null,
440
+ collectionPermissions: null
448
441
  }));
449
442
  }
450
443
 
@@ -452,37 +445,39 @@ export class UserService implements UserRepository {
452
445
  * Get role IDs for a user
453
446
  */
454
447
  async getUserRoleIds(userId: string): Promise<string[]> {
455
- const roles = await this.getUserRoles(userId);
456
- return roles.map(r => r.id);
448
+ const usersTableName = this.getQualifiedUsersTableName();
449
+ const result = await this.db.execute(sql`
450
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
451
+ `);
452
+
453
+ if (result.rows.length === 0) return [];
454
+
455
+ const row = result.rows[0] as { roles: string[] | null };
456
+ return row.roles ?? [];
457
457
  }
458
458
 
459
459
  /**
460
- * Set roles for a user
460
+ * Set roles for a user (replaces existing roles)
461
461
  */
462
462
  async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
463
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
464
- // Delete existing roles
465
- await this.db.execute(sql`DELETE FROM ${sql.raw(`"${rolesSchema}"."user_roles"`)} WHERE user_id = ${userId}`);
466
-
467
- // Insert new roles
468
- for (const roleId of roleIds) {
469
- await this.db.execute(sql`
470
- INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
471
- VALUES (${userId}, ${roleId})
472
- ON CONFLICT DO NOTHING
473
- `);
474
- }
463
+ const usersTableName = this.getQualifiedUsersTableName();
464
+ const rolesArray = `{${roleIds.join(",")}}`;
465
+ await this.db.execute(sql`
466
+ UPDATE ${sql.raw(usersTableName)}
467
+ SET roles = ${rolesArray}::text[], updated_at = NOW()
468
+ WHERE id = ${userId}
469
+ `);
475
470
  }
476
471
 
477
472
  /**
478
- * Assign a specific role to new user
473
+ * Assign a specific role to new user (appends if not present)
479
474
  */
480
475
  async assignDefaultRole(userId: string, roleId: string): Promise<void> {
481
- const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
476
+ const usersTableName = this.getQualifiedUsersTableName();
482
477
  await this.db.execute(sql`
483
- INSERT INTO ${sql.raw(`"${rolesSchema}"."user_roles"`)} (user_id, role_id)
484
- VALUES (${userId}, ${roleId})
485
- ON CONFLICT DO NOTHING
478
+ UPDATE ${sql.raw(usersTableName)}
479
+ SET roles = array_append(roles, ${roleId}), updated_at = NOW()
480
+ WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
486
481
  `);
487
482
  }
488
483
 
@@ -499,114 +494,7 @@ export class UserService implements UserRepository {
499
494
  }
500
495
  }
501
496
 
502
- /**
503
- * PostgreSQL implementation of RoleRepository.
504
- * Handles all role-related database operations using Drizzle ORM.
505
- */
506
- export class RoleService implements RoleRepository {
507
- private rolesTable: PgTable & Record<string, AnyPgColumn>;
508
-
509
- constructor(
510
- private db: NodePgDatabase,
511
- tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
512
- ) {
513
- if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).roles || (tableOrTables as Partial<AuthSchemaTables>).users)) {
514
- this.rolesTable = ((tableOrTables as Partial<AuthSchemaTables>).roles || roles) as unknown as PgTable & Record<string, AnyPgColumn>;
515
- } else {
516
- this.rolesTable = (tableOrTables as unknown as PgTable & Record<string, AnyPgColumn>) || (roles as unknown as PgTable & Record<string, AnyPgColumn>);
517
- }
518
- }
519
-
520
- private getQualifiedRolesTableName(): string {
521
- const name = getTableName(this.rolesTable);
522
- const schema = getTableConfig(this.rolesTable).schema || "public";
523
- return `"${schema}"."${name}"`;
524
- }
525
-
526
- async getRoleById(id: string): Promise<Role | null> {
527
- const tableName = this.getQualifiedRolesTableName();
528
- const result = await this.db.execute(sql`
529
- SELECT id, name, is_admin, default_permissions, collection_permissions
530
- FROM ${sql.raw(tableName)}
531
- WHERE id = ${id}
532
- `);
533
-
534
- if (result.rows.length === 0) return null;
535
-
536
- const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null };
537
- return {
538
- id: row.id,
539
- name: row.name,
540
- isAdmin: row.is_admin,
541
- defaultPermissions: row.default_permissions,
542
- collectionPermissions: row.collection_permissions
543
- };
544
- }
545
-
546
- async listRoles(): Promise<Role[]> {
547
- const tableName = this.getQualifiedRolesTableName();
548
- const result = await this.db.execute(sql`
549
- SELECT id, name, is_admin, default_permissions, collection_permissions
550
- FROM ${sql.raw(tableName)}
551
- ORDER BY name
552
- `);
553
-
554
- return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null }>).map(row => ({
555
- id: row.id,
556
- name: row.name,
557
- isAdmin: row.is_admin,
558
- defaultPermissions: row.default_permissions,
559
- collectionPermissions: row.collection_permissions
560
- }));
561
- }
562
-
563
- async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
564
- const tableName = this.getQualifiedRolesTableName();
565
- const result = await this.db.execute(sql`
566
- INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
567
- VALUES (
568
- ${data.id},
569
- ${data.name},
570
- ${data.isAdmin ?? false},
571
- ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
572
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
573
- )
574
- RETURNING id, name, is_admin, default_permissions, collection_permissions
575
- `);
576
-
577
- const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null };
578
- return {
579
- id: row.id,
580
- name: row.name,
581
- isAdmin: row.is_admin,
582
- defaultPermissions: row.default_permissions,
583
- collectionPermissions: row.collection_permissions
584
- };
585
- }
586
-
587
- async updateRole(id: string, data: Partial<Omit<Role, "id">>): Promise<Role | null> {
588
- const existing = await this.getRoleById(id);
589
- if (!existing) return null;
590
-
591
- const tableName = this.getQualifiedRolesTableName();
592
- await this.db.execute(sql`
593
- UPDATE ${sql.raw(tableName)}
594
- SET
595
- name = ${data.name ?? existing.name},
596
- is_admin = ${data.isAdmin ?? existing.isAdmin},
597
- default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
598
- collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb
599
- WHERE id = ${id}
600
- `);
601
-
602
- return this.getRoleById(id);
603
- }
604
497
 
605
- async deleteRole(id: string): Promise<void> {
606
- const tableName = this.getQualifiedRolesTableName();
607
- await this.db.execute(sql`DELETE FROM ${sql.raw(tableName)} WHERE id = ${id}`);
608
- }
609
- }
610
498
 
611
499
  export class RefreshTokenService {
612
500
  private refreshTokensTable: PgTable & Record<string, AnyPgColumn>;
@@ -878,7 +766,6 @@ export class PostgresTokenRepository implements TokenRepository {
878
766
  */
879
767
  export class PostgresAuthRepository implements AuthRepository {
880
768
  private userService: UserService;
881
- private roleService: RoleService;
882
769
  private tokenRepository: PostgresTokenRepository;
883
770
 
884
771
  constructor(
@@ -886,7 +773,6 @@ export class PostgresAuthRepository implements AuthRepository {
886
773
  tableOrTables?: (PgTable & Record<string, AnyPgColumn>) | Partial<AuthSchemaTables>
887
774
  ) {
888
775
  this.userService = new UserService(db, tableOrTables);
889
- this.roleService = new RoleService(db, tableOrTables);
890
776
  this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
891
777
  }
892
778
 
@@ -968,30 +854,48 @@ export class PostgresAuthRepository implements AuthRepository {
968
854
  return this.userService.getUserWithRoles(userId);
969
855
  }
970
856
 
971
- // Role operations (delegate to RoleService)
857
+ // Role operations (roles are inline on users, synthesized from string IDs)
972
858
 
973
859
  async getRoleById(id: string): Promise<RoleData | null> {
974
- return this.roleService.getRoleById(id);
860
+ return {
861
+ id,
862
+ name: id,
863
+ isAdmin: id === "admin",
864
+ defaultPermissions: null,
865
+ collectionPermissions: null
866
+ };
975
867
  }
976
868
 
977
869
  async listRoles(): Promise<RoleData[]> {
978
- return this.roleService.listRoles();
870
+ return [
871
+ { id: "admin", name: "Admin", isAdmin: true, defaultPermissions: null, collectionPermissions: null },
872
+ { id: "editor", name: "Editor", isAdmin: false, defaultPermissions: null, collectionPermissions: null },
873
+ { id: "viewer", name: "Viewer", isAdmin: false, defaultPermissions: null, collectionPermissions: null }
874
+ ];
979
875
  }
980
876
 
981
- async createRole(data: CreateRoleData): Promise<RoleData> {
982
- return this.roleService.createRole({
983
- ...data,
984
- defaultPermissions: data.defaultPermissions ?? null,
985
- collectionPermissions: data.collectionPermissions ?? null
986
- });
877
+ async createRole(_data: CreateRoleData): Promise<RoleData> {
878
+ return {
879
+ id: _data.id,
880
+ name: _data.name,
881
+ isAdmin: _data.isAdmin ?? false,
882
+ defaultPermissions: _data.defaultPermissions ?? null,
883
+ collectionPermissions: _data.collectionPermissions ?? null
884
+ };
987
885
  }
988
886
 
989
887
  async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
990
- return this.roleService.updateRole(id, data);
888
+ return {
889
+ id,
890
+ name: data.name ?? id,
891
+ isAdmin: data.isAdmin ?? (id === "admin"),
892
+ defaultPermissions: data.defaultPermissions ?? null,
893
+ collectionPermissions: data.collectionPermissions ?? null
894
+ };
991
895
  }
992
896
 
993
- async deleteRole(id: string): Promise<void> {
994
- await this.roleService.deleteRole(id);
897
+ async deleteRole(_id: string): Promise<void> {
898
+ // No-op: roles are inline strings on users
995
899
  }
996
900
 
997
901
  // Token operations (delegate to PostgresTokenRepository)
@@ -1314,6 +1218,3 @@ export class MfaService implements MfaRepository {
1314
1218
 
1315
1219
  /** PostgreSQL user repository implementation */
1316
1220
  export type PostgresUserRepository = UserService;
1317
-
1318
- /** PostgreSQL role repository implementation */
1319
- export type PostgresRoleRepository = RoleService;