@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
@@ -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.delegate.deleteAll(path);
1083
+ }
1084
+
1075
1085
  async checkUniqueField(
1076
1086
  path: string,
1077
1087
  name: string,
@@ -30,7 +30,7 @@ import {
30
30
  // @ts-ignore
31
31
  } from "@rebasepro/server-core";
32
32
  import { ensureAuthTablesExist } from "./auth/ensure-tables";
33
- import { RoleService, UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
33
+ import { UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
34
34
  import { createAuthSchema } from "./schema/auth-schema";
35
35
 
36
36
  // @ts-ignore
@@ -180,47 +180,51 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
180
180
  },
181
181
 
182
182
  async initializeAuth(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined> {
183
- const authConfig = config as AuthConfig | undefined;
183
+ const authConfig = config as Record<string, unknown> | undefined;
184
184
  if (!authConfig) return undefined;
185
185
 
186
186
  const internals = driverResult.internals as PostgresDriverInternals;
187
187
  const db = internals.db;
188
188
  const registry = internals.registry;
189
189
 
190
- await ensureAuthTablesExist(db, registry);
190
+ // Resolve the auth collection from the explicit config.
191
+ // This replaces the old `registry.getTable("users")` magic string lookup.
192
+ const authCollection = authConfig.collection as EntityCollection | undefined;
193
+
194
+ // ensureAuthTablesExist works with the collection abstraction — no Drizzle leakage.
195
+ await ensureAuthTablesExist(db, authCollection);
191
196
 
192
197
  let emailService: EmailService | undefined;
193
198
  if (authConfig.email) {
194
- emailService = createEmailService(authConfig.email);
199
+ emailService = createEmailService(authConfig.email as EmailConfig);
195
200
  }
196
201
 
197
- const customUsersTable = registry?.getTable("users");
198
- const customRolesTable = registry?.getTable("roles");
202
+ // Resolve the Drizzle table for the internal UserService/AuthRepository.
203
+ // These are internal Postgres-specific services that need the Drizzle table reference.
204
+ const tableName = authCollection
205
+ ? ("table" in authCollection && typeof authCollection.table === "string"
206
+ ? authCollection.table
207
+ : authCollection.slug)
208
+ : undefined;
209
+ const usersTable = tableName
210
+ ? registry.getTable(tableName) as (PgTable & Record<string, AnyPgColumn>) | undefined
211
+ : undefined;
199
212
 
200
213
  let usersSchemaName = "rebase";
201
- let rolesSchemaName = "rebase";
202
-
203
- if (customUsersTable) {
204
- usersSchemaName = getTableConfig(customUsersTable).schema || "public";
205
- }
206
- if (customRolesTable) {
207
- rolesSchemaName = getTableConfig(customRolesTable).schema || "public";
214
+ if (authCollection && "schema" in authCollection && typeof authCollection.schema === "string") {
215
+ usersSchemaName = authCollection.schema;
208
216
  }
209
217
 
210
- const authTables = createAuthSchema(rolesSchemaName, usersSchemaName) as unknown as AuthSchemaTables;
211
- if (customUsersTable) {
212
- authTables.users = customUsersTable as unknown as PgTable & Record<string, AnyPgColumn>;
213
- }
214
- if (customRolesTable) {
215
- authTables.roles = customRolesTable as unknown as PgTable & Record<string, AnyPgColumn>;
218
+ const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;
219
+ if (usersTable) {
220
+ authTables.users = usersTable as unknown as PgTable & Record<string, AnyPgColumn>;
216
221
  }
217
222
 
218
223
  const userService = new UserService(db, authTables);
219
- const roleService = new RoleService(db, authTables);
220
224
  const authRepository = new PostgresAuthRepository(db, authTables);
221
225
 
222
226
  return { userService,
223
- roleService,
227
+ roleService: userService,
224
228
  emailService,
225
229
  authRepository };
226
230
  },
@@ -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
- * This runs on startup to ensure the database is ready for auth
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, registry?: PostgresCollectionRegistry): Promise<void> {
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 = "public";
44
- if (registry) {
45
- const usersTable = registry.getTable("users") as (PgTable & Record<string, AnyPgColumn>) | undefined;
46
- if (usersTable) {
47
- const { getTableName } = await import("drizzle-orm");
48
- usersSchema = getTableConfig(usersTable).schema || "public";
49
- usersTableName = usersSchema === "public" ? `"${getTableName(usersTable)}"` : `"${usersSchema}"."${getTableName(usersTable)}"`;
50
-
51
- // Inspect users.id column to match referenced column type
52
- if (usersTable.id) {
53
- const col = usersTable.id;
54
- const meta = getColumnMeta(col);
55
- const columnType = meta.columnType;
56
- if (columnType === "PgUUID") {
57
- userIdType = "UUID";
58
- } else if (columnType === "PgSerial" || columnType === "PgInteger") {
59
- userIdType = "INTEGER";
60
- } else if (columnType === "PgBigInt" || columnType === "PgBigSerial") {
61
- userIdType = "BIGINT";
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
- // Resolve dynamic roles schema name
68
- let rolesSchema = "rebase";
69
- if (registry) {
70
- const rolesTable = registry.getTable("roles");
71
- if (rolesTable) {
72
- rolesSchema = getTableConfig(rolesTable).schema || "public";
73
- }
74
- }
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
- // Dynamic table names
86
- const userIdentitiesTable = `"${rolesSchema}"."user_identities"`;
87
- const rolesTableName = `"${rolesSchema}"."roles"`;
88
- const userRolesTableName = `"${rolesSchema}"."user_roles"`;
89
- const refreshTokensTableName = `"${rolesSchema}"."refresh_tokens"`;
90
- const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
91
- const appConfigTableName = `"${rolesSchema}"."app_config"`;
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
- await seedDefaultRoles(db, rolesTableName);
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 = `"${rolesSchema}"."mfa_factors"`;
242
- const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
243
- const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
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
- }