@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.
- package/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/index.es.js +301 -500
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +297 -496
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
- package/dist/server-postgresql/src/auth/services.d.ts +6 -31
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +21 -3
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +9 -5
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +10 -0
- package/src/PostgresBootstrapper.ts +25 -21
- package/src/auth/ensure-tables.ts +82 -129
- package/src/auth/services.ts +71 -170
- package/src/schema/auth-schema.ts +13 -69
- package/src/schema/doctor.ts +44 -3
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-drizzle-schema.ts +2 -6
- package/src/schema/introspect-db-logic.ts +7 -0
- package/src/services/EntityFetchService.ts +13 -1
- package/src/services/EntityPersistService.ts +9 -0
- package/src/services/entityService.ts +7 -0
- package/src/utils/drizzle-conditions.ts +40 -5
- package/src/websocket.ts +1 -3
- package/test/auth-services.test.ts +7 -150
- package/test/doctor.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +315 -0
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -14
- package/src/schema/default-collections.ts +0 -69
package/src/auth/services.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
425
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
435
426
|
const result = await this.db.execute(sql`
|
|
436
|
-
SELECT
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
456
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
await this.db.execute(sql`
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
476
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
482
477
|
await this.db.execute(sql`
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
982
|
-
return
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
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(
|
|
994
|
-
|
|
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,
|
|
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(
|
|
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
|
|
12
|
-
const usersTableCreator =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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"
|
|
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;
|
package/src/schema/doctor.ts
CHANGED
|
@@ -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
|
|
90
|
-
|
|
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",
|