@rebasepro/server-postgresql 0.3.0 → 0.4.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 (49) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +5 -8
  2. package/dist/common/src/data/query_builder.d.ts +6 -2
  3. package/dist/index.es.js +301 -500
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +297 -496
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
  8. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
  9. package/dist/server-postgresql/src/auth/services.d.ts +6 -31
  10. package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
  11. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
  12. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  13. package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
  14. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
  15. package/dist/types/src/controllers/auth.d.ts +2 -2
  16. package/dist/types/src/controllers/client.d.ts +25 -40
  17. package/dist/types/src/controllers/data.d.ts +21 -3
  18. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  19. package/dist/types/src/controllers/email.d.ts +2 -0
  20. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  21. package/dist/types/src/types/backend.d.ts +2 -2
  22. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  23. package/dist/types/src/types/collections.d.ts +9 -5
  24. package/dist/types/src/types/entity_views.d.ts +19 -28
  25. package/dist/types/src/types/properties.d.ts +9 -7
  26. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  27. package/dist/types/src/users/index.d.ts +0 -1
  28. package/dist/types/src/users/user.d.ts +0 -1
  29. package/package.json +6 -6
  30. package/src/PostgresBackendDriver.ts +10 -0
  31. package/src/PostgresBootstrapper.ts +25 -21
  32. package/src/auth/ensure-tables.ts +82 -129
  33. package/src/auth/services.ts +71 -170
  34. package/src/schema/auth-schema.ts +13 -69
  35. package/src/schema/doctor.ts +44 -3
  36. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  37. package/src/schema/generate-drizzle-schema.ts +2 -6
  38. package/src/schema/introspect-db-logic.ts +7 -0
  39. package/src/services/EntityFetchService.ts +13 -1
  40. package/src/services/EntityPersistService.ts +9 -0
  41. package/src/services/entityService.ts +7 -0
  42. package/src/utils/drizzle-conditions.ts +40 -5
  43. package/src/websocket.ts +1 -3
  44. package/test/auth-services.test.ts +7 -150
  45. package/test/doctor.test.ts +6 -2
  46. package/test/relation-pipeline-gaps.test.ts +315 -0
  47. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  48. package/dist/types/src/users/roles.d.ts +0 -14
  49. package/src/schema/default-collections.ts +0 -69
@@ -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;
@@ -1,15 +1,14 @@
1
- import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
1
+ import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique } from "drizzle-orm/pg-core";
2
2
  import { relations } from "drizzle-orm";
3
3
 
4
4
  /**
5
5
  * Factory function to dynamically create the auth tables bound to the specified schema names.
6
6
  */
7
- export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchemaName: string = "rebase") {
8
- const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
7
+ export function createAuthSchema(usersSchemaName: string = "rebase") {
9
8
  const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
10
9
 
11
- const rolesTableCreator = (rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable) as typeof pgTable;
12
- const usersTableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
10
+ const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
11
+ const usersTableCreator = tableCreator;
13
12
 
14
13
  /**
15
14
  * Users table - stores both email/password and OAuth users
@@ -24,48 +23,18 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
24
23
  emailVerificationToken: varchar("email_verification_token", { length: 255 }),
25
24
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
26
25
  isAnonymous: boolean("is_anonymous").default(false).notNull(),
26
+ roles: text("roles").array().default([]).notNull(),
27
27
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
28
28
  createdAt: timestamp("created_at").defaultNow().notNull(),
29
29
  updatedAt: timestamp("updated_at").defaultNow().notNull()
30
30
  });
31
31
 
32
- /**
33
- * Roles table - defines permission sets
34
- */
35
- const roles = rolesTableCreator("roles", {
36
- id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
37
- name: varchar("name", { length: 100 }).notNull(),
38
- isAdmin: boolean("is_admin").default(false).notNull(),
39
- defaultPermissions: jsonb("default_permissions").$type<{
40
- read?: boolean;
41
- create?: boolean;
42
- edit?: boolean;
43
- delete?: boolean;
44
- }>(),
45
- collectionPermissions: jsonb("collection_permissions").$type<
46
- Record<string, {
47
- read?: boolean;
48
- create?: boolean;
49
- edit?: boolean;
50
- delete?: boolean;
51
- }>
52
- >()
53
- });
54
32
 
55
- /**
56
- * User-Role junction table
57
- */
58
- const userRoles = rolesTableCreator("user_roles", {
59
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
60
- roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
61
- }, (table) => ({
62
- pk: primaryKey({ columns: [table.userId, table.roleId] })
63
- }));
64
33
 
65
34
  /**
66
35
  * Refresh tokens for long-lived sessions
67
36
  */
68
- const refreshTokens = rolesTableCreator("refresh_tokens", {
37
+ const refreshTokens = tableCreator("refresh_tokens", {
69
38
  id: uuid("id").defaultRandom().primaryKey(),
70
39
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
71
40
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
@@ -80,7 +49,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
80
49
  /**
81
50
  * Password reset tokens for forgot password flow
82
51
  */
83
- const passwordResetTokens = rolesTableCreator("password_reset_tokens", {
52
+ const passwordResetTokens = tableCreator("password_reset_tokens", {
84
53
  id: uuid("id").defaultRandom().primaryKey(),
85
54
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
86
55
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
@@ -92,7 +61,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
92
61
  /**
93
62
  * App config - key/value store for custom settings
94
63
  */
95
- const appConfig = rolesTableCreator("app_config", {
64
+ const appConfig = tableCreator("app_config", {
96
65
  key: varchar("key", { length: 100 }).primaryKey(),
97
66
  value: jsonb("value").notNull(),
98
67
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -101,7 +70,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
101
70
  /**
102
71
  * User identities - maps external OAuth profiles back to local users
103
72
  */
104
- const userIdentities = rolesTableCreator("user_identities", {
73
+ const userIdentities = tableCreator("user_identities", {
105
74
  id: uuid("id").defaultRandom().primaryKey(),
106
75
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
107
76
  provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
@@ -116,7 +85,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
116
85
  /**
117
86
  * MFA factors table - stores enrolled MFA methods
118
87
  */
119
- const mfaFactors = rolesTableCreator("mfa_factors", {
88
+ const mfaFactors = tableCreator("mfa_factors", {
120
89
  id: uuid("id").defaultRandom().primaryKey(),
121
90
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
122
91
  factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
@@ -130,7 +99,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
130
99
  /**
131
100
  * MFA challenges table - tracks active MFA verification attempts
132
101
  */
133
- const mfaChallenges = rolesTableCreator("mfa_challenges", {
102
+ const mfaChallenges = tableCreator("mfa_challenges", {
134
103
  id: uuid("id").defaultRandom().primaryKey(),
135
104
  factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
136
105
  createdAt: timestamp("created_at").defaultNow().notNull(),
@@ -142,7 +111,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
142
111
  /**
143
112
  * Recovery codes table - backup codes for MFA
144
113
  */
145
- const recoveryCodes = rolesTableCreator("recovery_codes", {
114
+ const recoveryCodes = tableCreator("recovery_codes", {
146
115
  id: uuid("id").defaultRandom().primaryKey(),
147
116
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
148
117
  codeHash: varchar("code_hash", { length: 255 }).notNull(),
@@ -151,11 +120,8 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
151
120
  });
152
121
 
153
122
  return {
154
- rolesSchema,
155
123
  usersSchema,
156
124
  users,
157
- roles,
158
- userRoles,
159
125
  refreshTokens,
160
126
  passwordResetTokens,
161
127
  appConfig,
@@ -167,14 +133,11 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
167
133
  }
168
134
 
169
135
  // Instantiate default schema and tables using the default "rebase" schema
170
- const defaultAuthSchema = createAuthSchema("rebase", "rebase");
136
+ const defaultAuthSchema = createAuthSchema("rebase");
171
137
 
172
- export const rebaseSchema = defaultAuthSchema.rolesSchema;
173
138
  export const usersSchema = defaultAuthSchema.usersSchema;
174
139
 
175
140
  export const users = defaultAuthSchema.users;
176
- export const roles = defaultAuthSchema.roles;
177
- export const userRoles = defaultAuthSchema.userRoles;
178
141
  export const refreshTokens = defaultAuthSchema.refreshTokens;
179
142
  export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
180
143
  export const appConfig = defaultAuthSchema.appConfig;
@@ -185,7 +148,6 @@ export const recoveryCodes = defaultAuthSchema.recoveryCodes;
185
148
 
186
149
  // Relations
187
150
  export const usersRelations = relations(users, ({ many }) => ({
188
- userRoles: many(userRoles),
189
151
  refreshTokens: many(refreshTokens),
190
152
  passwordResetTokens: many(passwordResetTokens),
191
153
  userIdentities: many(userIdentities),
@@ -193,21 +155,6 @@ export const usersRelations = relations(users, ({ many }) => ({
193
155
  recoveryCodes: many(recoveryCodes)
194
156
  }));
195
157
 
196
- export const rolesRelations = relations(roles, ({ many }) => ({
197
- userRoles: many(userRoles)
198
- }));
199
-
200
- export const userRolesRelations = relations(userRoles, ({ one }) => ({
201
- user: one(users, {
202
- fields: [userRoles.userId],
203
- references: [users.id]
204
- }),
205
- role: one(roles, {
206
- fields: [userRoles.roleId],
207
- references: [roles.id]
208
- })
209
- }));
210
-
211
158
  export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
212
159
  user: one(users, {
213
160
  fields: [refreshTokens.userId],
@@ -254,9 +201,6 @@ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
254
201
  // Type exports
255
202
  export type User = typeof users.$inferSelect;
256
203
  export type NewUser = typeof users.$inferInsert;
257
- export type Role = typeof roles.$inferSelect;
258
- export type NewRole = typeof roles.$inferInsert;
259
- export type UserRole = typeof userRoles.$inferSelect;
260
204
  export type RefreshToken = typeof refreshTokens.$inferSelect;
261
205
  export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
262
206
  export type AppConfig = typeof appConfig.$inferSelect;
@@ -84,10 +84,28 @@ export function getExpectedColumnType(prop: Property): string | null {
84
84
  if (dp.columnType === "time") return "time without time zone";
85
85
  return "timestamp with time zone";
86
86
  }
87
- case "map":
88
87
  case "array": {
89
- const ap = prop as ArrayProperty | MapProperty;
90
- if (ap.columnType === "json") return "json";
88
+ const ap = prop as ArrayProperty;
89
+ let colType = ap.columnType;
90
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
91
+ const ofProp = ap.of as Property;
92
+ if (ofProp.type === "string") {
93
+ colType = "text[]";
94
+ } else if (ofProp.type === "number") {
95
+ colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
96
+ } else if (ofProp.type === "boolean") {
97
+ colType = "boolean[]";
98
+ }
99
+ }
100
+
101
+ if (colType === "json") return "json";
102
+ if (colType === "jsonb") return "jsonb";
103
+ if (colType && colType.endsWith("[]")) return "ARRAY";
104
+ return "jsonb";
105
+ }
106
+ case "map": {
107
+ const mp = prop as MapProperty;
108
+ if (mp.columnType === "json") return "json";
91
109
  return "jsonb";
92
110
  }
93
111
  case "relation":
@@ -485,6 +503,29 @@ export async function checkCollectionsVsDatabase(
485
503
  if (prop.type === "vector" && dbCol.udt_name !== "vector") {
486
504
  isMismatch = true;
487
505
  }
506
+ if (prop.type === "array") {
507
+ const ap = prop as ArrayProperty;
508
+ let expectedColType = ap.columnType;
509
+ if (!expectedColType && ap.of && !Array.isArray(ap.of)) {
510
+ const ofProp = ap.of as Property;
511
+ if (ofProp.type === "string") expectedColType = "text[]";
512
+ else if (ofProp.type === "number") expectedColType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
513
+ else if (ofProp.type === "boolean") expectedColType = "boolean[]";
514
+ }
515
+ if (expectedColType && expectedColType.endsWith("[]")) {
516
+ if (actualType !== "ARRAY") {
517
+ isMismatch = true;
518
+ } else {
519
+ const expectedUdt = expectedColType === "text[]" ? "_text" :
520
+ expectedColType === "integer[]" ? "_int4" :
521
+ expectedColType === "boolean[]" ? "_bool" :
522
+ expectedColType === "numeric[]" ? "_numeric" : "";
523
+ if (expectedUdt && dbCol.udt_name !== expectedUdt) {
524
+ isMismatch = true;
525
+ }
526
+ }
527
+ }
528
+ }
488
529
  if (isMismatch) {
489
530
  issues.push({
490
531
  severity: "warning",