@rebasepro/server-postgresql 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -89
- package/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/common/src/util/permissions.d.ts +14 -6
- package/dist/index.es.js +379 -611
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +375 -607
- 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 +17 -42
- package/dist/server-postgresql/src/data-transformer.d.ts +0 -3
- package/dist/server-postgresql/src/databasePoolManager.d.ts +1 -1
- 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/types.d.ts +3 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
- package/dist/server-postgresql/src/websocket.d.ts +8 -3
- 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 +38 -3
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +30 -6
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +9 -15
- 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 +27 -22
- package/src/auth/ensure-tables.ts +82 -129
- package/src/auth/services.ts +99 -197
- package/src/cli.ts +50 -23
- package/src/data-transformer.ts +57 -95
- package/src/databasePoolManager.ts +2 -1
- 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 +38 -12
- package/src/services/entityService.ts +7 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +40 -5
- package/src/websocket.ts +38 -25
- 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/drizzle.test.config.ts +0 -10
- package/src/schema/default-collections.ts +0 -69
package/src/auth/services.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { eq, getTableName, sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { getTableConfig
|
|
4
|
-
import {
|
|
3
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
4
|
+
import type { RebasePgTable } from "../types";
|
|
5
|
+
import { users, refreshTokens, passwordResetTokens, userIdentities } from "../schema/auth-schema";
|
|
5
6
|
import {
|
|
6
7
|
UserRepository,
|
|
7
8
|
RoleRepository,
|
|
@@ -27,16 +28,14 @@ import { toSnakeCase, camelCase } from "@rebasepro/utils";
|
|
|
27
28
|
export type { Role };
|
|
28
29
|
|
|
29
30
|
export interface AuthSchemaTables {
|
|
30
|
-
users:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
appConfig: PgTable & Record<string, AnyPgColumn>;
|
|
36
|
-
userIdentities: PgTable & Record<string, AnyPgColumn>;
|
|
31
|
+
users: RebasePgTable;
|
|
32
|
+
refreshTokens: RebasePgTable;
|
|
33
|
+
passwordResetTokens: RebasePgTable;
|
|
34
|
+
appConfig: RebasePgTable;
|
|
35
|
+
userIdentities: RebasePgTable;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
function getColumnKey(table:
|
|
38
|
+
function getColumnKey(table: RebasePgTable | undefined, ...keys: string[]): string | undefined {
|
|
40
39
|
if (!table) return undefined;
|
|
41
40
|
for (const key of keys) {
|
|
42
41
|
if (key in table) return key;
|
|
@@ -48,7 +47,7 @@ function getColumnKey(table: (PgTable & Record<string, AnyPgColumn>) | undefined
|
|
|
48
47
|
return undefined;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
function getColumn(table:
|
|
50
|
+
function getColumn(table: RebasePgTable | undefined, ...keys: string[]): RebasePgTable[string] | undefined {
|
|
52
51
|
if (!table) return undefined;
|
|
53
52
|
const key = getColumnKey(table, ...keys);
|
|
54
53
|
return key ? table[key] : undefined;
|
|
@@ -59,27 +58,21 @@ function getColumn(table: (PgTable & Record<string, AnyPgColumn>) | undefined, .
|
|
|
59
58
|
* Handles all user-related database operations using Drizzle ORM.
|
|
60
59
|
*/
|
|
61
60
|
export class UserService implements UserRepository {
|
|
62
|
-
private usersTable:
|
|
63
|
-
private userIdentitiesTable:
|
|
64
|
-
private userRolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
65
|
-
private rolesTable: PgTable & Record<string, AnyPgColumn>;
|
|
61
|
+
private usersTable: RebasePgTable;
|
|
62
|
+
private userIdentitiesTable: RebasePgTable;
|
|
66
63
|
|
|
67
64
|
constructor(
|
|
68
65
|
private db: NodePgDatabase,
|
|
69
|
-
tableOrTables?:
|
|
66
|
+
tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>
|
|
70
67
|
) {
|
|
71
|
-
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users
|
|
68
|
+
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
72
69
|
const tables = tableOrTables as Partial<AuthSchemaTables>;
|
|
73
|
-
this.usersTable = (tables.users || users) as
|
|
74
|
-
this.userIdentitiesTable = (tables.userIdentities || userIdentities) as
|
|
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>;
|
|
70
|
+
this.usersTable = (tables.users || users) as RebasePgTable;
|
|
71
|
+
this.userIdentitiesTable = (tables.userIdentities || userIdentities) as RebasePgTable;
|
|
77
72
|
} else {
|
|
78
|
-
const table = tableOrTables as
|
|
79
|
-
this.usersTable = table || (users as unknown as
|
|
80
|
-
this.userIdentitiesTable = userIdentities as unknown as
|
|
81
|
-
this.userRolesTable = userRoles as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
82
|
-
this.rolesTable = roles as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
73
|
+
const table = tableOrTables as RebasePgTable | undefined;
|
|
74
|
+
this.usersTable = table || (users as unknown as RebasePgTable);
|
|
75
|
+
this.userIdentitiesTable = userIdentities as unknown as RebasePgTable;
|
|
83
76
|
}
|
|
84
77
|
}
|
|
85
78
|
|
|
@@ -90,7 +83,7 @@ export class UserService implements UserRepository {
|
|
|
90
83
|
}
|
|
91
84
|
|
|
92
85
|
private mapRowToUser(row: Record<string, unknown>): UserData {
|
|
93
|
-
if (!row) return row as
|
|
86
|
+
if (!row) return row as UserData;
|
|
94
87
|
|
|
95
88
|
const id = (row.id ?? row.uid) as string;
|
|
96
89
|
const email = row.email as string;
|
|
@@ -115,6 +108,7 @@ export class UserService implements UserRepository {
|
|
|
115
108
|
"email_verification_token", "emailVerificationToken",
|
|
116
109
|
"email_verification_sent_at", "emailVerificationSentAt",
|
|
117
110
|
"is_anonymous", "isAnonymous",
|
|
111
|
+
"roles",
|
|
118
112
|
"created_at", "createdAt",
|
|
119
113
|
"updated_at", "updatedAt",
|
|
120
114
|
"metadata"
|
|
@@ -315,11 +309,9 @@ export class UserService implements UserRepository {
|
|
|
315
309
|
const idColumn = idCol ? idCol.name : "id";
|
|
316
310
|
|
|
317
311
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
318
|
-
const rolesSchema = getTableConfig(this.userRolesTable).schema || "public";
|
|
319
|
-
|
|
320
312
|
const conditions = [];
|
|
321
313
|
if (roleId) {
|
|
322
|
-
conditions.push(sql
|
|
314
|
+
conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);
|
|
323
315
|
}
|
|
324
316
|
if (search) {
|
|
325
317
|
const pattern = `%${search}%`;
|
|
@@ -331,7 +323,7 @@ export class UserService implements UserRepository {
|
|
|
331
323
|
// Sorting: users with roles first if no role filter, then by requested order
|
|
332
324
|
const orderByClause = roleId
|
|
333
325
|
? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`
|
|
334
|
-
: sql`ORDER BY (
|
|
326
|
+
: sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;
|
|
335
327
|
|
|
336
328
|
const countResult = await this.db.execute(sql`
|
|
337
329
|
SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}
|
|
@@ -428,23 +420,25 @@ export class UserService implements UserRepository {
|
|
|
428
420
|
}
|
|
429
421
|
|
|
430
422
|
/**
|
|
431
|
-
* Get roles for a user from database
|
|
423
|
+
* Get roles for a user from database (inline TEXT[] column)
|
|
432
424
|
*/
|
|
433
425
|
async getUserRoles(userId: string): Promise<Role[]> {
|
|
434
|
-
const
|
|
426
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
435
427
|
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}
|
|
428
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
|
|
440
429
|
`);
|
|
441
430
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
431
|
+
if (result.rows.length === 0) return [];
|
|
432
|
+
|
|
433
|
+
const row = result.rows[0] as { roles: string[] | null };
|
|
434
|
+
const roleIds = row.roles ?? [];
|
|
435
|
+
|
|
436
|
+
return roleIds.map(id => ({
|
|
437
|
+
id,
|
|
438
|
+
name: id,
|
|
439
|
+
isAdmin: id === "admin",
|
|
440
|
+
defaultPermissions: null,
|
|
441
|
+
collectionPermissions: null
|
|
448
442
|
}));
|
|
449
443
|
}
|
|
450
444
|
|
|
@@ -452,37 +446,39 @@ export class UserService implements UserRepository {
|
|
|
452
446
|
* Get role IDs for a user
|
|
453
447
|
*/
|
|
454
448
|
async getUserRoleIds(userId: string): Promise<string[]> {
|
|
455
|
-
const
|
|
456
|
-
|
|
449
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
450
|
+
const result = await this.db.execute(sql`
|
|
451
|
+
SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
|
|
452
|
+
`);
|
|
453
|
+
|
|
454
|
+
if (result.rows.length === 0) return [];
|
|
455
|
+
|
|
456
|
+
const row = result.rows[0] as { roles: string[] | null };
|
|
457
|
+
return row.roles ?? [];
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
/**
|
|
460
|
-
* Set roles for a user
|
|
461
|
+
* Set roles for a user (replaces existing roles)
|
|
461
462
|
*/
|
|
462
463
|
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
|
-
}
|
|
464
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
465
|
+
const rolesArray = `{${roleIds.join(",")}}`;
|
|
466
|
+
await this.db.execute(sql`
|
|
467
|
+
UPDATE ${sql.raw(usersTableName)}
|
|
468
|
+
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
469
|
+
WHERE id = ${userId}
|
|
470
|
+
`);
|
|
475
471
|
}
|
|
476
472
|
|
|
477
473
|
/**
|
|
478
|
-
* Assign a specific role to new user
|
|
474
|
+
* Assign a specific role to new user (appends if not present)
|
|
479
475
|
*/
|
|
480
476
|
async assignDefaultRole(userId: string, roleId: string): Promise<void> {
|
|
481
|
-
const
|
|
477
|
+
const usersTableName = this.getQualifiedUsersTableName();
|
|
482
478
|
await this.db.execute(sql`
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
479
|
+
UPDATE ${sql.raw(usersTableName)}
|
|
480
|
+
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
481
|
+
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
486
482
|
`);
|
|
487
483
|
}
|
|
488
484
|
|
|
@@ -499,126 +495,19 @@ export class UserService implements UserRepository {
|
|
|
499
495
|
}
|
|
500
496
|
}
|
|
501
497
|
|
|
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
498
|
|
|
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
|
-
|
|
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
499
|
|
|
611
500
|
export class RefreshTokenService {
|
|
612
|
-
private refreshTokensTable:
|
|
501
|
+
private refreshTokensTable: RebasePgTable;
|
|
613
502
|
|
|
614
503
|
constructor(
|
|
615
504
|
private db: NodePgDatabase,
|
|
616
|
-
tableOrTables?:
|
|
505
|
+
tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>
|
|
617
506
|
) {
|
|
618
507
|
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
619
|
-
this.refreshTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || refreshTokens) as
|
|
508
|
+
this.refreshTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || refreshTokens) as RebasePgTable;
|
|
620
509
|
} else {
|
|
621
|
-
this.refreshTokensTable = (tableOrTables as
|
|
510
|
+
this.refreshTokensTable = (tableOrTables as RebasePgTable) || (refreshTokens as unknown as RebasePgTable);
|
|
622
511
|
}
|
|
623
512
|
}
|
|
624
513
|
|
|
@@ -707,16 +596,16 @@ export class RefreshTokenService {
|
|
|
707
596
|
* Password reset token service
|
|
708
597
|
*/
|
|
709
598
|
export class PasswordResetTokenService {
|
|
710
|
-
private passwordResetTokensTable:
|
|
599
|
+
private passwordResetTokensTable: RebasePgTable;
|
|
711
600
|
|
|
712
601
|
constructor(
|
|
713
602
|
private db: NodePgDatabase,
|
|
714
|
-
tableOrTables?:
|
|
603
|
+
tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>
|
|
715
604
|
) {
|
|
716
605
|
if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {
|
|
717
|
-
this.passwordResetTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || passwordResetTokens) as
|
|
606
|
+
this.passwordResetTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || passwordResetTokens) as RebasePgTable;
|
|
718
607
|
} else {
|
|
719
|
-
this.passwordResetTokensTable = (tableOrTables as
|
|
608
|
+
this.passwordResetTokensTable = (tableOrTables as RebasePgTable) || (passwordResetTokens as unknown as RebasePgTable);
|
|
720
609
|
}
|
|
721
610
|
}
|
|
722
611
|
|
|
@@ -816,7 +705,7 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
816
705
|
|
|
817
706
|
constructor(
|
|
818
707
|
private db: NodePgDatabase,
|
|
819
|
-
tableOrTables?:
|
|
708
|
+
tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>
|
|
820
709
|
) {
|
|
821
710
|
this.refreshTokenService = new RefreshTokenService(db, tableOrTables);
|
|
822
711
|
this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
|
|
@@ -878,15 +767,13 @@ export class PostgresTokenRepository implements TokenRepository {
|
|
|
878
767
|
*/
|
|
879
768
|
export class PostgresAuthRepository implements AuthRepository {
|
|
880
769
|
private userService: UserService;
|
|
881
|
-
private roleService: RoleService;
|
|
882
770
|
private tokenRepository: PostgresTokenRepository;
|
|
883
771
|
|
|
884
772
|
constructor(
|
|
885
773
|
private db: NodePgDatabase,
|
|
886
|
-
tableOrTables?:
|
|
774
|
+
tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>
|
|
887
775
|
) {
|
|
888
776
|
this.userService = new UserService(db, tableOrTables);
|
|
889
|
-
this.roleService = new RoleService(db, tableOrTables);
|
|
890
777
|
this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);
|
|
891
778
|
}
|
|
892
779
|
|
|
@@ -968,30 +855,48 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
968
855
|
return this.userService.getUserWithRoles(userId);
|
|
969
856
|
}
|
|
970
857
|
|
|
971
|
-
// Role operations (
|
|
858
|
+
// Role operations (roles are inline on users, synthesized from string IDs)
|
|
972
859
|
|
|
973
860
|
async getRoleById(id: string): Promise<RoleData | null> {
|
|
974
|
-
return
|
|
861
|
+
return {
|
|
862
|
+
id,
|
|
863
|
+
name: id,
|
|
864
|
+
isAdmin: id === "admin",
|
|
865
|
+
defaultPermissions: null,
|
|
866
|
+
collectionPermissions: null
|
|
867
|
+
};
|
|
975
868
|
}
|
|
976
869
|
|
|
977
870
|
async listRoles(): Promise<RoleData[]> {
|
|
978
|
-
return
|
|
871
|
+
return [
|
|
872
|
+
{ id: "admin", name: "Admin", isAdmin: true, defaultPermissions: null, collectionPermissions: null },
|
|
873
|
+
{ id: "editor", name: "Editor", isAdmin: false, defaultPermissions: null, collectionPermissions: null },
|
|
874
|
+
{ id: "viewer", name: "Viewer", isAdmin: false, defaultPermissions: null, collectionPermissions: null }
|
|
875
|
+
];
|
|
979
876
|
}
|
|
980
877
|
|
|
981
|
-
async createRole(
|
|
982
|
-
return
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
878
|
+
async createRole(_data: CreateRoleData): Promise<RoleData> {
|
|
879
|
+
return {
|
|
880
|
+
id: _data.id,
|
|
881
|
+
name: _data.name,
|
|
882
|
+
isAdmin: _data.isAdmin ?? false,
|
|
883
|
+
defaultPermissions: _data.defaultPermissions ?? null,
|
|
884
|
+
collectionPermissions: _data.collectionPermissions ?? null
|
|
885
|
+
};
|
|
987
886
|
}
|
|
988
887
|
|
|
989
888
|
async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
|
|
990
|
-
return
|
|
889
|
+
return {
|
|
890
|
+
id,
|
|
891
|
+
name: data.name ?? id,
|
|
892
|
+
isAdmin: data.isAdmin ?? (id === "admin"),
|
|
893
|
+
defaultPermissions: data.defaultPermissions ?? null,
|
|
894
|
+
collectionPermissions: data.collectionPermissions ?? null
|
|
895
|
+
};
|
|
991
896
|
}
|
|
992
897
|
|
|
993
|
-
async deleteRole(
|
|
994
|
-
|
|
898
|
+
async deleteRole(_id: string): Promise<void> {
|
|
899
|
+
// No-op: roles are inline strings on users
|
|
995
900
|
}
|
|
996
901
|
|
|
997
902
|
// Token operations (delegate to PostgresTokenRepository)
|
|
@@ -1314,6 +1219,3 @@ export class MfaService implements MfaRepository {
|
|
|
1314
1219
|
|
|
1315
1220
|
/** PostgreSQL user repository implementation */
|
|
1316
1221
|
export type PostgresUserRepository = UserService;
|
|
1317
|
-
|
|
1318
|
-
/** PostgreSQL role repository implementation */
|
|
1319
|
-
export type PostgresRoleRepository = RoleService;
|
package/src/cli.ts
CHANGED
|
@@ -436,15 +436,28 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
436
436
|
// dotenv may not be available — fall through
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
const interactive = ["generate", "push"].includes(action);
|
|
439
|
+
const interactive = ["generate", "push"].includes(action) && Boolean(process.stdout.isTTY);
|
|
440
440
|
|
|
441
441
|
// For push: always use --strict (prompts before destructive ops) and --verbose
|
|
442
442
|
// (shows all SQL). This ensures unmapped tables are never silently dropped.
|
|
443
443
|
const drizzleKitArgs = [action];
|
|
444
444
|
if (action === "push") {
|
|
445
445
|
drizzleKitArgs.push("--strict", "--verbose");
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Forward any additional arguments, excluding schema-generator-specific options
|
|
449
|
+
const excludedFlags = ["--collections", "-c", "--output", "-o", "--watch", "-w"];
|
|
450
|
+
for (let i = 2; i < _rawArgs.length; i++) {
|
|
451
|
+
const arg = _rawArgs[i];
|
|
452
|
+
if (excludedFlags.includes(arg)) {
|
|
453
|
+
// Skip this flag and its value if it takes a parameter
|
|
454
|
+
if (["--collections", "-c", "--output", "-o"].includes(arg)) {
|
|
455
|
+
i++; // Skip the next arg (the value)
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!drizzleKitArgs.includes(arg)) {
|
|
460
|
+
drizzleKitArgs.push(arg);
|
|
448
461
|
}
|
|
449
462
|
}
|
|
450
463
|
|
|
@@ -456,7 +469,7 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
456
469
|
env
|
|
457
470
|
});
|
|
458
471
|
} else {
|
|
459
|
-
const child = execa(drizzleKitBin,
|
|
472
|
+
const child = execa(drizzleKitBin, drizzleKitArgs, {
|
|
460
473
|
cwd: process.cwd(),
|
|
461
474
|
env,
|
|
462
475
|
reject: false
|
|
@@ -473,20 +486,28 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
473
486
|
const stdout = stripAnsi(result.stdout || "").trim();
|
|
474
487
|
const stderr = stripAnsi(result.stderr || "").trim();
|
|
475
488
|
|
|
476
|
-
|
|
489
|
+
const hasTtyError = stdout.includes("Interactive prompts require a TTY terminal") ||
|
|
490
|
+
stderr.includes("Interactive prompts require a TTY terminal");
|
|
491
|
+
|
|
492
|
+
if (result.exitCode !== 0 || hasTtyError) {
|
|
477
493
|
console.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
if (hasTtyError) {
|
|
495
|
+
console.error(chalk.red(" Error: Interactive prompts require a TTY terminal."));
|
|
496
|
+
console.error(chalk.gray(" Please run with --force to skip interactive prompts or run in an interactive terminal."));
|
|
497
|
+
} else {
|
|
498
|
+
const errorOutput = stderr || stdout;
|
|
499
|
+
if (errorOutput) {
|
|
500
|
+
const lines = errorOutput.split("\n").filter((l: string) => l.trim());
|
|
501
|
+
let printedCount = 0;
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
|
|
504
|
+
console.error(chalk.red(` ${line.trim()}`));
|
|
505
|
+
printedCount++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (printedCount === 0) {
|
|
509
|
+
lines.slice(0, 10).forEach(line => console.error(chalk.red(` ${line.trim()}`)));
|
|
486
510
|
}
|
|
487
|
-
}
|
|
488
|
-
if (printedCount === 0) {
|
|
489
|
-
lines.slice(0, 10).forEach(line => console.error(chalk.red(` ${line.trim()}`)));
|
|
490
511
|
}
|
|
491
512
|
}
|
|
492
513
|
console.error("");
|
|
@@ -498,15 +519,21 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
498
519
|
// eslint-disable-next-line no-control-regex
|
|
499
520
|
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\[?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣷⣯⣟⡿⢿⣻⣽]+\]\s*/g, "");
|
|
500
521
|
const cleaned = stripAnsi(msg).trim();
|
|
522
|
+
const hasTtyError = cleaned.includes("Interactive prompts require a TTY terminal");
|
|
501
523
|
console.error(chalk.red(`\n✗ drizzle-kit ${action} failed.\n`));
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
524
|
+
if (hasTtyError) {
|
|
525
|
+
console.error(chalk.red(" Error: Interactive prompts require a TTY terminal."));
|
|
526
|
+
console.error(chalk.gray(" Please run with --force to skip interactive prompts or run in an interactive terminal."));
|
|
527
|
+
} else {
|
|
528
|
+
const lines = cleaned.split("\n").filter((l: string) => l.trim());
|
|
529
|
+
for (const line of lines) {
|
|
530
|
+
if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates")) {
|
|
531
|
+
console.error(chalk.red(` ${line.trim()}`));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (lines.length === 0) {
|
|
535
|
+
console.error(chalk.gray(` ${cleaned}`));
|
|
506
536
|
}
|
|
507
|
-
}
|
|
508
|
-
if (lines.length === 0) {
|
|
509
|
-
console.error(chalk.gray(` ${cleaned}`));
|
|
510
537
|
}
|
|
511
538
|
console.error("");
|
|
512
539
|
process.exit(1);
|