@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentRef } from "./component_ref";
|
|
2
|
-
import type { EntityReference, EntityRelation, EntityValues, GeoPoint,
|
|
3
|
-
import type {
|
|
2
|
+
import type { Entity, EntityReference, EntityRelation, EntityValues, GeoPoint, Vector } from "./entities";
|
|
3
|
+
import type { JoinStep, OnAction, Relation } from "./relations";
|
|
4
4
|
import type { EntityCollection, FilterValues } from "./collections";
|
|
5
5
|
import type { ColorKey, ColorScheme } from "./chips";
|
|
6
6
|
import type { AuthController } from "../controllers/auth";
|
|
@@ -104,8 +104,8 @@ export interface BaseUIConfig<CustomProps = unknown> {
|
|
|
104
104
|
disabled?: boolean | PropertyDisabledConfig;
|
|
105
105
|
widthPercentage?: number;
|
|
106
106
|
customProps?: CustomProps;
|
|
107
|
-
Field?: ComponentRef
|
|
108
|
-
Preview?: ComponentRef
|
|
107
|
+
Field?: ComponentRef<any>;
|
|
108
|
+
Preview?: ComponentRef<any>;
|
|
109
109
|
}
|
|
110
110
|
export interface BaseProperty<CustomProps = unknown> {
|
|
111
111
|
ui?: BaseUIConfig<CustomProps>;
|
|
@@ -185,7 +185,7 @@ export interface StringProperty extends BaseProperty {
|
|
|
185
185
|
* Optional database column type. If not set, it defaults to `varchar` or `uuid` depending on `isId` configuration.
|
|
186
186
|
* Use `text` for strings with unbound length, `char` for fixed-length strings, or `varchar` for variable-length strings with a limit.
|
|
187
187
|
*/
|
|
188
|
-
columnType?: "varchar" | "text" | "char";
|
|
188
|
+
columnType?: "varchar" | "text" | "char" | "uuid";
|
|
189
189
|
/**
|
|
190
190
|
* Rules for validating this property
|
|
191
191
|
*/
|
|
@@ -541,9 +541,11 @@ export interface ArrayProperty extends BaseProperty {
|
|
|
541
541
|
ui?: ArrayUIConfig;
|
|
542
542
|
type: "array";
|
|
543
543
|
/**
|
|
544
|
-
* Optional database column type.
|
|
544
|
+
* Optional database column type. By default, maps to a native Postgres array
|
|
545
|
+
* (e.g. `text[]`, `integer[]`/`numeric[]`, `boolean[]`) if the element type
|
|
546
|
+
* is a primitive, otherwise defaults to `jsonb`.
|
|
545
547
|
*/
|
|
546
|
-
columnType?: "json" | "jsonb";
|
|
548
|
+
columnType?: "json" | "jsonb" | "text[]" | "integer[]" | "boolean[]" | "numeric[]";
|
|
547
549
|
/**
|
|
548
550
|
* The property of this array.
|
|
549
551
|
* You can specify any property (except another Array property)
|
|
@@ -639,14 +641,6 @@ export interface MapProperty extends BaseProperty {
|
|
|
639
641
|
* Properties that are displayed when rendered as a preview
|
|
640
642
|
*/
|
|
641
643
|
previewProperties?: string[];
|
|
642
|
-
/**
|
|
643
|
-
* Allow the user to add only some keys in this map.
|
|
644
|
-
* By default, all properties of the map have the corresponding field in
|
|
645
|
-
* the form view. Setting this flag to true allows to pick only some.
|
|
646
|
-
* Useful for map that can have a lot of sub-properties that may not be
|
|
647
|
-
* needed
|
|
648
|
-
*/
|
|
649
|
-
pickOnlySomeKeys?: boolean;
|
|
650
644
|
/**
|
|
651
645
|
* Render this map as a key-value table that allows to use
|
|
652
646
|
* arbitrary keys. You don't need to define the properties in this case.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { User } from "../users";
|
|
2
2
|
/**
|
|
3
3
|
* Result of creating a new user via admin flow.
|
|
4
4
|
* Contains the created user plus information about how credentials were delivered.
|
|
@@ -15,56 +15,46 @@ export interface UserCreationResult<USER extends User = User> {
|
|
|
15
15
|
temporaryPassword?: string;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Delegate to manage
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Delegate to manage auth-specific user operations.
|
|
19
|
+
*
|
|
20
|
+
* This interface allows the CMS to be agnostic of the underlying
|
|
21
|
+
* authentication provider or backend. User/role CRUD is now handled
|
|
22
|
+
* by the collection system; this delegate only exposes auth-specific
|
|
23
|
+
* operations (password hashing, invitations, bootstrap).
|
|
21
24
|
*
|
|
22
25
|
* @group Models
|
|
23
26
|
*/
|
|
24
27
|
export interface UserManagementDelegate<USER extends User = User> {
|
|
25
28
|
/**
|
|
26
|
-
* Are
|
|
29
|
+
* Are auth-related operations currently loading?
|
|
27
30
|
*/
|
|
28
31
|
loading: boolean;
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
33
|
+
* In-memory list of users (used for client-side filtering fallback).
|
|
31
34
|
*/
|
|
32
|
-
users
|
|
35
|
+
users?: USER[];
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
37
|
+
* Error from fetching the users list, if any.
|
|
35
38
|
*/
|
|
36
39
|
usersError?: Error;
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
39
|
-
* user information when assigning ownership of an entity.
|
|
40
|
-
* @param uid
|
|
41
|
+
* Look up a single user by UID from the in-memory cache.
|
|
41
42
|
*/
|
|
42
|
-
getUser
|
|
43
|
+
getUser?: (uid: string) => USER | null;
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
* When provided, the CMS will use this for the users table
|
|
46
|
-
* instead of loading all users into memory.
|
|
45
|
+
* Server-side user search with pagination.
|
|
47
46
|
*/
|
|
48
|
-
searchUsers?: (
|
|
47
|
+
searchUsers?: (params: {
|
|
49
48
|
search?: string;
|
|
50
49
|
limit?: number;
|
|
51
50
|
offset?: number;
|
|
52
|
-
orderBy?: string;
|
|
53
|
-
orderDir?: "asc" | "desc";
|
|
54
|
-
roleId?: string;
|
|
55
51
|
}) => Promise<{
|
|
56
52
|
users: USER[];
|
|
57
53
|
total: number;
|
|
58
54
|
}>;
|
|
59
|
-
/**
|
|
60
|
-
* Save a user (create or update)
|
|
61
|
-
* @param user
|
|
62
|
-
*/
|
|
63
|
-
saveUser?: (user: USER) => Promise<USER>;
|
|
64
55
|
/**
|
|
65
56
|
* Create a new user with invitation/password generation support.
|
|
66
57
|
* Returns additional info about how the credentials were delivered.
|
|
67
|
-
* Falls back to saveUser if not provided.
|
|
68
58
|
*/
|
|
69
59
|
createUser?: (user: USER) => Promise<UserCreationResult<USER>>;
|
|
70
60
|
/**
|
|
@@ -73,42 +63,15 @@ export interface UserManagementDelegate<USER extends User = User> {
|
|
|
73
63
|
* or a flag indicating an email invitation was sent.
|
|
74
64
|
*/
|
|
75
65
|
resetPassword?: (user: USER) => Promise<UserCreationResult<USER>>;
|
|
76
|
-
/**
|
|
77
|
-
* Delete a user
|
|
78
|
-
* @param user
|
|
79
|
-
*/
|
|
80
|
-
deleteUser?: (user: USER) => Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* List of roles defined in the CMS.
|
|
83
|
-
*/
|
|
84
|
-
roles?: Role[];
|
|
85
|
-
/**
|
|
86
|
-
* Optional error if roles failed to load.
|
|
87
|
-
*/
|
|
88
|
-
rolesError?: Error;
|
|
89
|
-
/**
|
|
90
|
-
* Save a role (create or update)
|
|
91
|
-
* @param role
|
|
92
|
-
*/
|
|
93
|
-
saveRole?: (role: Role) => Promise<void>;
|
|
94
|
-
/**
|
|
95
|
-
* Delete a role
|
|
96
|
-
* @param role
|
|
97
|
-
*/
|
|
98
|
-
deleteRole?: (role: Role) => Promise<void>;
|
|
99
66
|
/**
|
|
100
67
|
* Is the currently logged in user an admin?
|
|
101
68
|
*/
|
|
102
69
|
isAdmin?: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* If true, the UI will allow the user to create the default roles (admin, editor, viewer).
|
|
105
|
-
*/
|
|
106
|
-
allowDefaultRolesCreation?: boolean;
|
|
107
70
|
/**
|
|
108
71
|
* Optionally define roles for a given user. This is useful when the roles
|
|
109
72
|
* are coming from a separate provider than the one issuing the tokens.
|
|
110
73
|
*/
|
|
111
|
-
defineRolesFor?: (user: USER) => Promise<
|
|
74
|
+
defineRolesFor?: (user: USER) => Promise<string[] | undefined> | string[] | undefined;
|
|
112
75
|
/**
|
|
113
76
|
* Whether any admin users exist. Used by the bootstrap banner to decide
|
|
114
77
|
* whether to prompt. Populated via a lightweight check (e.g. `limit=1`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgresql",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -68,11 +68,11 @@
|
|
|
68
68
|
"hono": "^4.12.21",
|
|
69
69
|
"pg": "^8.21.0",
|
|
70
70
|
"ws": "^8.20.1",
|
|
71
|
-
"@rebasepro/
|
|
72
|
-
"@rebasepro/
|
|
73
|
-
"@rebasepro/
|
|
74
|
-
"@rebasepro/utils": "0.
|
|
75
|
-
"@rebasepro/
|
|
71
|
+
"@rebasepro/sdk-generator": "0.5.0",
|
|
72
|
+
"@rebasepro/server-core": "0.5.0",
|
|
73
|
+
"@rebasepro/types": "0.5.0",
|
|
74
|
+
"@rebasepro/utils": "0.5.0",
|
|
75
|
+
"@rebasepro/common": "0.5.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/jest": "^29.5.14",
|
|
@@ -631,6 +631,12 @@ propertyCallbacks: undefined };
|
|
|
631
631
|
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
async deleteAll(path: string): Promise<void> {
|
|
635
|
+
await this.entityService.deleteAll(path);
|
|
636
|
+
// Notify real-time subscribers of bulk change
|
|
637
|
+
await this.realtimeService.notifyEntityUpdate(path, "*", null);
|
|
638
|
+
}
|
|
639
|
+
|
|
634
640
|
async checkUniqueField(
|
|
635
641
|
path: string,
|
|
636
642
|
name: string,
|
|
@@ -1072,6 +1078,10 @@ roles: this.user?.roles ?? [] };
|
|
|
1072
1078
|
return this.withTransaction((delegate) => delegate.deleteEntity(props));
|
|
1073
1079
|
}
|
|
1074
1080
|
|
|
1081
|
+
async deleteAll(path: string): Promise<void> {
|
|
1082
|
+
return this.withTransaction((delegate) => delegate.deleteAll(path));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1075
1085
|
async checkUniqueField(
|
|
1076
1086
|
path: string,
|
|
1077
1087
|
name: string,
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { getTableName, isTable, Relations, sql, Table } from "drizzle-orm";
|
|
8
8
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
9
|
-
import { PgEnum, PgTable, getTableConfig
|
|
9
|
+
import { PgEnum, PgTable, getTableConfig } from "drizzle-orm/pg-core";
|
|
10
|
+
import type { RebasePgTable } from "./types";
|
|
10
11
|
import {
|
|
11
12
|
BackendBootstrapper,
|
|
12
13
|
InitializedDriver,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
// @ts-ignore
|
|
31
32
|
} from "@rebasepro/server-core";
|
|
32
33
|
import { ensureAuthTablesExist } from "./auth/ensure-tables";
|
|
33
|
-
import {
|
|
34
|
+
import { UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
|
|
34
35
|
import { createAuthSchema } from "./schema/auth-schema";
|
|
35
36
|
|
|
36
37
|
// @ts-ignore
|
|
@@ -180,47 +181,51 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
180
181
|
},
|
|
181
182
|
|
|
182
183
|
async initializeAuth(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined> {
|
|
183
|
-
const authConfig = config as
|
|
184
|
+
const authConfig = config as Record<string, unknown> | undefined;
|
|
184
185
|
if (!authConfig) return undefined;
|
|
185
186
|
|
|
186
187
|
const internals = driverResult.internals as PostgresDriverInternals;
|
|
187
188
|
const db = internals.db;
|
|
188
189
|
const registry = internals.registry;
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
// Resolve the auth collection from the explicit config.
|
|
192
|
+
// This replaces the old `registry.getTable("users")` magic string lookup.
|
|
193
|
+
const authCollection = authConfig.collection as EntityCollection | undefined;
|
|
194
|
+
|
|
195
|
+
// ensureAuthTablesExist works with the collection abstraction — no Drizzle leakage.
|
|
196
|
+
await ensureAuthTablesExist(db, authCollection);
|
|
191
197
|
|
|
192
198
|
let emailService: EmailService | undefined;
|
|
193
199
|
if (authConfig.email) {
|
|
194
|
-
emailService = createEmailService(authConfig.email);
|
|
200
|
+
emailService = createEmailService(authConfig.email as EmailConfig);
|
|
195
201
|
}
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
// Resolve the Drizzle table for the internal UserService/AuthRepository.
|
|
204
|
+
// These are internal Postgres-specific services that need the Drizzle table reference.
|
|
205
|
+
const tableName = authCollection
|
|
206
|
+
? ("table" in authCollection && typeof authCollection.table === "string"
|
|
207
|
+
? authCollection.table
|
|
208
|
+
: authCollection.slug)
|
|
209
|
+
: undefined;
|
|
210
|
+
const usersTable = tableName
|
|
211
|
+
? registry.getTable(tableName) as RebasePgTable | undefined
|
|
212
|
+
: undefined;
|
|
199
213
|
|
|
200
214
|
let usersSchemaName = "rebase";
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (customUsersTable) {
|
|
204
|
-
usersSchemaName = getTableConfig(customUsersTable).schema || "public";
|
|
205
|
-
}
|
|
206
|
-
if (customRolesTable) {
|
|
207
|
-
rolesSchemaName = getTableConfig(customRolesTable).schema || "public";
|
|
215
|
+
if (authCollection && "schema" in authCollection && typeof authCollection.schema === "string") {
|
|
216
|
+
usersSchemaName = authCollection.schema;
|
|
208
217
|
}
|
|
209
218
|
|
|
210
|
-
const authTables = createAuthSchema(
|
|
211
|
-
if (
|
|
212
|
-
authTables.users =
|
|
213
|
-
}
|
|
214
|
-
if (customRolesTable) {
|
|
215
|
-
authTables.roles = customRolesTable as unknown as PgTable & Record<string, AnyPgColumn>;
|
|
219
|
+
const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;
|
|
220
|
+
if (usersTable) {
|
|
221
|
+
authTables.users = usersTable as RebasePgTable;
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
const userService = new UserService(db, authTables);
|
|
219
|
-
const roleService = new RoleService(db, authTables);
|
|
220
225
|
const authRepository = new PostgresAuthRepository(db, authTables);
|
|
221
226
|
|
|
222
227
|
return { userService,
|
|
223
|
-
roleService,
|
|
228
|
+
roleService: userService,
|
|
224
229
|
emailService,
|
|
225
230
|
authRepository };
|
|
226
231
|
},
|
|
@@ -1,94 +1,62 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { getTableConfig, AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
4
|
-
import { getColumnMeta } from "../services/entity-helpers";
|
|
5
|
-
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
3
|
import { logger } from "@rebasepro/server-core";
|
|
4
|
+
import type { EntityCollection } from "@rebasepro/types";
|
|
5
|
+
|
|
7
6
|
|
|
8
|
-
/**
|
|
9
|
-
* Default roles to seed on first run
|
|
10
|
-
*/
|
|
11
|
-
const DEFAULT_ROLES = [
|
|
12
|
-
{
|
|
13
|
-
id: "admin",
|
|
14
|
-
name: "Admin",
|
|
15
|
-
is_admin: true,
|
|
16
|
-
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: "editor",
|
|
20
|
-
name: "Editor",
|
|
21
|
-
is_admin: false,
|
|
22
|
-
default_permissions: { read: true, create: true, edit: true, delete: true }
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: "viewer",
|
|
26
|
-
name: "Viewer",
|
|
27
|
-
is_admin: false,
|
|
28
|
-
default_permissions: { read: true, create: false, edit: false, delete: false }
|
|
29
|
-
}
|
|
30
|
-
];
|
|
31
7
|
|
|
32
8
|
/**
|
|
33
|
-
* Auto-create auth tables if they don't exist
|
|
34
|
-
*
|
|
9
|
+
* Auto-create auth tables if they don't exist.
|
|
10
|
+
*
|
|
11
|
+
* @param db — Drizzle database instance
|
|
12
|
+
* @param collection — The collection that represents auth users.
|
|
13
|
+
* When omitted, a default `rebase.users` table is created.
|
|
35
14
|
*/
|
|
36
|
-
export async function ensureAuthTablesExist(db: NodePgDatabase,
|
|
15
|
+
export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: EntityCollection): Promise<void> {
|
|
37
16
|
logger.info("🔍 Checking auth tables...");
|
|
38
17
|
|
|
39
18
|
try {
|
|
40
|
-
// Resolve dynamic user table name and ID type
|
|
41
|
-
let usersTableName = '"users"';
|
|
19
|
+
// Resolve dynamic user table name and ID type from the collection
|
|
20
|
+
let usersTableName = '"rebase"."users"';
|
|
42
21
|
let userIdType = "TEXT";
|
|
43
|
-
let usersSchema = "
|
|
44
|
-
if (
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
22
|
+
let usersSchema = "rebase";
|
|
23
|
+
if (collection) {
|
|
24
|
+
const rawTable = ("table" in collection && typeof collection.table === "string")
|
|
25
|
+
? collection.table
|
|
26
|
+
: collection.slug;
|
|
27
|
+
usersSchema = ("schema" in collection && typeof collection.schema === "string")
|
|
28
|
+
? collection.schema
|
|
29
|
+
: "public";
|
|
30
|
+
usersTableName = usersSchema === "public"
|
|
31
|
+
? `"${rawTable}"`
|
|
32
|
+
: `"${usersSchema}"."${rawTable}"`;
|
|
33
|
+
|
|
34
|
+
// Derive ID column type from collection properties
|
|
35
|
+
const idProp = collection.properties?.id;
|
|
36
|
+
if (idProp) {
|
|
37
|
+
const isId = ("isId" in idProp) ? (idProp as unknown as Record<string, unknown>).isId : undefined;
|
|
38
|
+
if (isId === "uuid") {
|
|
39
|
+
userIdType = "UUID";
|
|
40
|
+
} else if (isId === "autoincrement") {
|
|
41
|
+
userIdType = "INTEGER";
|
|
63
42
|
}
|
|
43
|
+
// Otherwise keep TEXT as default
|
|
64
44
|
}
|
|
65
45
|
}
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
let rolesSchema = "rebase";
|
|
69
|
-
if (registry) {
|
|
70
|
-
const rolesTable = registry.getTable("roles");
|
|
71
|
-
if (rolesTable) {
|
|
72
|
-
rolesSchema = getTableConfig(rolesTable).schema || "public";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
47
|
+
|
|
75
48
|
|
|
76
49
|
// ── Create schemas (idempotent) ──────────────────────────────────
|
|
77
50
|
if (usersSchema !== "public") {
|
|
78
51
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);
|
|
79
52
|
}
|
|
80
|
-
if (rolesSchema !== "public" && rolesSchema !== usersSchema) {
|
|
81
|
-
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
|
|
82
|
-
}
|
|
83
53
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
84
54
|
|
|
85
|
-
|
|
86
|
-
const userIdentitiesTable = `"${
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
|
|
91
|
-
const appConfigTableName = `"${rolesSchema}"."app_config"`;
|
|
55
|
+
const authSchema = usersSchema === "public" ? "rebase" : usersSchema;
|
|
56
|
+
const userIdentitiesTable = `"${authSchema}"."user_identities"`;
|
|
57
|
+
const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
|
|
58
|
+
const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
|
|
59
|
+
const appConfigTableName = `"${authSchema}"."app_config"`;
|
|
92
60
|
|
|
93
61
|
// ── Create tables (idempotent) ──────────────────────────────────
|
|
94
62
|
|
|
@@ -113,32 +81,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
113
81
|
`);
|
|
114
82
|
|
|
115
83
|
|
|
116
|
-
// Create roles table
|
|
117
|
-
await db.execute(sql`
|
|
118
|
-
CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
|
|
119
|
-
id TEXT PRIMARY KEY,
|
|
120
|
-
name TEXT NOT NULL,
|
|
121
|
-
is_admin BOOLEAN DEFAULT FALSE,
|
|
122
|
-
default_permissions JSONB,
|
|
123
|
-
collection_permissions JSONB,
|
|
124
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
125
|
-
)
|
|
126
|
-
`);
|
|
127
|
-
|
|
128
|
-
// Create user_roles junction table
|
|
129
|
-
await db.execute(sql`
|
|
130
|
-
CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
|
|
131
|
-
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
132
|
-
role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
|
|
133
|
-
PRIMARY KEY (user_id, role_id)
|
|
134
|
-
)
|
|
135
|
-
`);
|
|
136
|
-
|
|
137
|
-
// Create index on user_id for faster lookups
|
|
138
|
-
await db.execute(sql`
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_user_roles_user
|
|
140
|
-
ON ${sql.raw(userRolesTableName)}(user_id)
|
|
141
|
-
`);
|
|
142
84
|
|
|
143
85
|
// Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
|
|
144
86
|
await db.execute(sql`
|
|
@@ -229,7 +171,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
229
171
|
});
|
|
230
172
|
|
|
231
173
|
// Seed default roles if none exist
|
|
232
|
-
|
|
174
|
+
// (no-op: roles are now stored inline on the users table)
|
|
233
175
|
|
|
234
176
|
// ── Migration: Add is_anonymous column (safe for existing tables) ────
|
|
235
177
|
await db.execute(sql`
|
|
@@ -237,10 +179,51 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
237
179
|
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
238
180
|
`);
|
|
239
181
|
|
|
182
|
+
// ── Migration: Add inline roles column (safe for existing tables) ────
|
|
183
|
+
await db.execute(sql`
|
|
184
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
185
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
// ── Migration: Copy roles from legacy junction table to inline column ──
|
|
189
|
+
// If the old rebase.user_roles and rebase.roles tables exist, migrate
|
|
190
|
+
// the data into the new TEXT[] column then drop the legacy tables.
|
|
191
|
+
try {
|
|
192
|
+
const legacyCheck = await db.execute(sql`
|
|
193
|
+
SELECT EXISTS (
|
|
194
|
+
SELECT 1 FROM information_schema.tables
|
|
195
|
+
WHERE table_schema = 'rebase' AND table_name = 'user_roles'
|
|
196
|
+
) AS has_user_roles
|
|
197
|
+
`);
|
|
198
|
+
const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;
|
|
199
|
+
|
|
200
|
+
if (hasLegacyTables) {
|
|
201
|
+
logger.info("🔄 Migrating roles from legacy user_roles table...");
|
|
202
|
+
// Update users' roles column from the junction table
|
|
203
|
+
await db.execute(sql`
|
|
204
|
+
UPDATE ${sql.raw(usersTableName)} u
|
|
205
|
+
SET roles = COALESCE((
|
|
206
|
+
SELECT array_agg(ur.role_id)
|
|
207
|
+
FROM "rebase"."user_roles" ur
|
|
208
|
+
WHERE ur.user_id = u.id
|
|
209
|
+
), '{}')
|
|
210
|
+
WHERE u.roles = '{}' OR u.roles IS NULL
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
// Drop legacy tables (junction first due to FK)
|
|
214
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
|
|
215
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
|
|
216
|
+
logger.info("✅ Legacy roles tables migrated and dropped");
|
|
217
|
+
}
|
|
218
|
+
} catch (migrationError: unknown) {
|
|
219
|
+
// Non-fatal: log and continue — the column exists and will work
|
|
220
|
+
logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
240
223
|
// ── MFA tables ──────────────────────────────────────────────────────
|
|
241
|
-
const mfaFactorsTableName = `"${
|
|
242
|
-
const mfaChallengesTableName = `"${
|
|
243
|
-
const recoveryCodesTableName = `"${
|
|
224
|
+
const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
|
|
225
|
+
const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
|
|
226
|
+
const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
|
|
244
227
|
|
|
245
228
|
// Create mfa_factors table
|
|
246
229
|
await db.execute(sql`
|
|
@@ -304,33 +287,3 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
|
|
|
304
287
|
}
|
|
305
288
|
}
|
|
306
289
|
|
|
307
|
-
/**
|
|
308
|
-
* Seed default roles if the roles table is empty
|
|
309
|
-
*/
|
|
310
|
-
async function seedDefaultRoles(db: NodePgDatabase, rolesTableName: string): Promise<void> {
|
|
311
|
-
// Check if any roles exist
|
|
312
|
-
const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
|
|
313
|
-
const count = parseInt((result.rows[0] as Record<string, string | number>)?.count as string || "0", 10);
|
|
314
|
-
|
|
315
|
-
if (count > 0) {
|
|
316
|
-
logger.info(`📋 Found ${count} existing roles`);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
logger.info("🌱 Seeding default roles...");
|
|
321
|
-
|
|
322
|
-
for (const role of DEFAULT_ROLES) {
|
|
323
|
-
await db.execute(sql`
|
|
324
|
-
INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
|
|
325
|
-
VALUES (
|
|
326
|
-
${role.id},
|
|
327
|
-
${role.name},
|
|
328
|
-
${role.is_admin},
|
|
329
|
-
${JSON.stringify(role.default_permissions)}::jsonb
|
|
330
|
-
)
|
|
331
|
-
ON CONFLICT (id) DO NOTHING
|
|
332
|
-
`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
logger.info("✅ Default roles created: admin, editor, viewer");
|
|
336
|
-
}
|