@rebasepro/server-postgresql 0.2.1 → 0.2.4

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 (57) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +12 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/data/query_builder.d.ts +51 -0
  4. package/dist/common/src/index.d.ts +1 -0
  5. package/dist/common/src/util/permissions.d.ts +1 -0
  6. package/dist/index.es.js +1202 -369
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +1200 -367
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
  11. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  12. package/dist/server-postgresql/src/auth/services.d.ts +43 -1
  13. package/dist/server-postgresql/src/connection.d.ts +25 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
  15. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  16. package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
  17. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  18. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  19. package/dist/types/src/controllers/auth.d.ts +2 -24
  20. package/dist/types/src/controllers/client.d.ts +0 -3
  21. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  22. package/dist/types/src/controllers/data.d.ts +21 -0
  23. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  24. package/dist/types/src/controllers/registry.d.ts +5 -4
  25. package/dist/types/src/rebase_context.d.ts +1 -1
  26. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  27. package/dist/types/src/types/collections.d.ts +0 -4
  28. package/dist/types/src/types/component_ref.d.ts +1 -1
  29. package/dist/types/src/types/cron.d.ts +1 -1
  30. package/dist/types/src/types/entity_views.d.ts +1 -0
  31. package/dist/types/src/types/export_import.d.ts +1 -1
  32. package/dist/types/src/types/formex.d.ts +2 -2
  33. package/dist/types/src/types/properties.d.ts +2 -2
  34. package/dist/types/src/types/translations.d.ts +28 -12
  35. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  36. package/dist/types/src/users/roles.d.ts +0 -8
  37. package/package.json +7 -6
  38. package/src/PostgresBackendDriver.ts +13 -7
  39. package/src/PostgresBootstrapper.ts +27 -8
  40. package/src/auth/ensure-tables.ts +79 -17
  41. package/src/auth/services.ts +292 -23
  42. package/src/cli.ts +5 -0
  43. package/src/connection.ts +77 -0
  44. package/src/data-transformer.ts +2 -2
  45. package/src/schema/auth-schema.ts +80 -14
  46. package/src/schema/default-collections.ts +1 -0
  47. package/src/schema/doctor.ts +82 -41
  48. package/src/schema/generate-drizzle-schema.ts +6 -6
  49. package/src/services/EntityFetchService.ts +69 -10
  50. package/src/services/entityService.ts +2 -0
  51. package/src/services/realtimeService.ts +214 -2
  52. package/src/utils/drizzle-conditions.ts +74 -2
  53. package/src/websocket.ts +10 -2
  54. package/test/auth-services.test.ts +15 -28
  55. package/test/drizzle-conditions.test.ts +168 -0
  56. package/test/postgresDataDriver.test.ts +130 -1
  57. package/vite.config.ts +1 -1
@@ -25,7 +25,8 @@ import {
25
25
  createAuthRoutes,
26
26
  createAdminRoutes,
27
27
  requireAuth,
28
- requireAdmin
28
+ requireAdmin,
29
+ logger
29
30
  // @ts-ignore
30
31
  } from "@rebasepro/server-core";
31
32
  import { ensureAuthTablesExist } from "./auth/ensure-tables";
@@ -50,6 +51,7 @@ import type { HonoEnv } from "@rebasepro/server-core";
50
51
  */
51
52
  export interface PostgresDriverInternals {
52
53
  db: NodePgDatabase<any>;
54
+ readDb?: NodePgDatabase<any>;
53
55
  registry: PostgresCollectionRegistry;
54
56
  realtimeService: RealtimeService;
55
57
  driver: PostgresBackendDriver;
@@ -83,7 +85,7 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
83
85
  const registry = new PostgresCollectionRegistry();
84
86
  if (collections) {
85
87
  registry.registerMultiple(collections);
86
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map(c => c.slug).join(", ")}]`);
88
+ logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map(c => c.slug).join(", ")}]`);
87
89
  }
88
90
 
89
91
  // Register tables
@@ -114,12 +116,26 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
114
116
  try {
115
117
  await schemaAwareDb.execute(sql`SELECT 1`);
116
118
  } catch (err) {
117
- console.error("❌ Failed to connect to PostgreSQL:", err);
118
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
119
+ logger.error("❌ Failed to connect to PostgreSQL", { error: err });
120
+ logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
119
121
  }
120
122
 
121
123
  // Create services
122
124
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
125
+
126
+ // Initialize read replica connection if configured
127
+ let readDb: import("drizzle-orm/node-postgres").NodePgDatabase<any> | undefined;
128
+ const readUrl = process.env.DATABASE_READ_URL;
129
+ if (readUrl && readUrl !== pgConfig.connectionString) {
130
+ try {
131
+ const { createReadReplicaConnection } = await import("./connection");
132
+ const readResources = createReadReplicaConnection(readUrl, mergedSchema);
133
+ readDb = readResources.db;
134
+ logger.info("📖 [PostgresBootstrapper] Read replica connection established");
135
+ } catch (err) {
136
+ logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", { error: err });
137
+ }
138
+ }
123
139
  const poolManager = pgConfig.adminConnectionString
124
140
  ? new DatabasePoolManager(pgConfig.adminConnectionString)
125
141
  : undefined;
@@ -131,21 +147,24 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
131
147
  try {
132
148
  await driver.branchService.ensureBranchMetadataTable();
133
149
  } catch (err) {
134
- console.warn("⚠️ Could not initialize branch metadata table:", err);
150
+ logger.warn("⚠️ Could not initialize branch metadata table", { error: err });
135
151
  }
136
152
  }
137
153
 
138
154
  // Enable cross-instance realtime (opt-in)
139
- if (pgConfig.connectionString) {
155
+ // Prefer DATABASE_DIRECT_URL to bypass PgBouncer for LISTEN/NOTIFY
156
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
157
+ if (directUrl) {
140
158
  try {
141
- await realtimeService.startListening(pgConfig.connectionString);
159
+ await realtimeService.startListening(directUrl);
142
160
  } catch (err) {
143
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
161
+ logger.warn("⚠️ Cross-instance realtime could not be started", { error: err });
144
162
  }
145
163
  }
146
164
 
147
165
  const internals: PostgresDriverInternals = {
148
166
  db: schemaAwareDb,
167
+ readDb,
149
168
  registry,
150
169
  realtimeService,
151
170
  driver,
@@ -3,6 +3,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
3
  import { getTableConfig, AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
4
4
  import { getColumnMeta } from "../services/entity-helpers";
5
5
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
+ import { logger } from "@rebasepro/server-core";
6
7
 
7
8
  /**
8
9
  * Default roles to seed on first run
@@ -12,22 +13,19 @@ const DEFAULT_ROLES = [
12
13
  id: "admin",
13
14
  name: "Admin",
14
15
  is_admin: true,
15
- default_permissions: { read: true, create: true, edit: true, delete: true },
16
- config: { createCollections: true, editCollections: "all", deleteCollections: "all" }
16
+ default_permissions: { read: true, create: true, edit: true, delete: true }
17
17
  },
18
18
  {
19
19
  id: "editor",
20
20
  name: "Editor",
21
21
  is_admin: false,
22
- default_permissions: { read: true, create: true, edit: true, delete: true },
23
- config: { createCollections: true, editCollections: "own", deleteCollections: "own" }
22
+ default_permissions: { read: true, create: true, edit: true, delete: true }
24
23
  },
25
24
  {
26
25
  id: "viewer",
27
26
  name: "Viewer",
28
27
  is_admin: false,
29
- default_permissions: { read: true, create: false, edit: false, delete: false },
30
- config: null
28
+ default_permissions: { read: true, create: false, edit: false, delete: false }
31
29
  }
32
30
  ];
33
31
 
@@ -36,7 +34,7 @@ const DEFAULT_ROLES = [
36
34
  * This runs on startup to ensure the database is ready for auth
37
35
  */
38
36
  export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: PostgresCollectionRegistry): Promise<void> {
39
- console.log("🔍 Checking auth tables...");
37
+ logger.info("🔍 Checking auth tables...");
40
38
 
41
39
  try {
42
40
  // Resolve dynamic user table name and ID type
@@ -123,7 +121,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
123
121
  is_admin BOOLEAN DEFAULT FALSE,
124
122
  default_permissions JSONB,
125
123
  collection_permissions JSONB,
126
- config JSONB,
127
124
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
128
125
  )
129
126
  `);
@@ -234,10 +231,76 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: Postg
234
231
  // Seed default roles if none exist
235
232
  await seedDefaultRoles(db, rolesTableName);
236
233
 
237
- console.log("✅ Auth tables ready");
234
+ // ── Migration: Add is_anonymous column (safe for existing tables) ────
235
+ await db.execute(sql`
236
+ ALTER TABLE ${sql.raw(usersTableName)}
237
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
238
+ `);
239
+
240
+ // ── MFA tables ──────────────────────────────────────────────────────
241
+ const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
242
+ const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
243
+ const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
244
+
245
+ // Create mfa_factors table
246
+ await db.execute(sql`
247
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
248
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
249
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
250
+ factor_type TEXT NOT NULL DEFAULT 'totp',
251
+ secret_encrypted TEXT NOT NULL,
252
+ friendly_name TEXT,
253
+ verified BOOLEAN DEFAULT FALSE,
254
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
255
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
256
+ )
257
+ `);
258
+
259
+ // Create indexes on mfa_factors
260
+ await db.execute(sql`
261
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
262
+ ON ${sql.raw(mfaFactorsTableName)}(user_id)
263
+ `);
264
+
265
+ // Create mfa_challenges table
266
+ await db.execute(sql`
267
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
268
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
269
+ factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
270
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
271
+ verified_at TIMESTAMP WITH TIME ZONE,
272
+ ip_address TEXT,
273
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
274
+ )
275
+ `);
276
+
277
+ // Create indexes on mfa_challenges
278
+ await db.execute(sql`
279
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
280
+ ON ${sql.raw(mfaChallengesTableName)}(factor_id)
281
+ `);
282
+
283
+ // Create recovery_codes table
284
+ await db.execute(sql`
285
+ CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
286
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
287
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
288
+ code_hash TEXT NOT NULL,
289
+ used_at TIMESTAMP WITH TIME ZONE,
290
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
291
+ )
292
+ `);
293
+
294
+ // Create indexes on recovery_codes
295
+ await db.execute(sql`
296
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
297
+ ON ${sql.raw(recoveryCodesTableName)}(user_id)
298
+ `);
299
+
300
+ logger.info("✅ Auth tables ready");
238
301
  } catch (error) {
239
- console.error("❌ Failed to create auth tables:", error);
240
- console.warn("⚠️ Continuing without creating auth tables.");
302
+ logger.error("❌ Failed to create auth tables", { error });
303
+ logger.warn("⚠️ Continuing without creating auth tables.");
241
304
  }
242
305
  }
243
306
 
@@ -250,25 +313,24 @@ async function seedDefaultRoles(db: NodePgDatabase, rolesTableName: string): Pro
250
313
  const count = parseInt((result.rows[0] as Record<string, string | number>)?.count as string || "0", 10);
251
314
 
252
315
  if (count > 0) {
253
- console.log(`📋 Found ${count} existing roles`);
316
+ logger.info(`📋 Found ${count} existing roles`);
254
317
  return;
255
318
  }
256
319
 
257
- console.log("🌱 Seeding default roles...");
320
+ logger.info("🌱 Seeding default roles...");
258
321
 
259
322
  for (const role of DEFAULT_ROLES) {
260
323
  await db.execute(sql`
261
- INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
324
+ INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
262
325
  VALUES (
263
326
  ${role.id},
264
327
  ${role.name},
265
328
  ${role.is_admin},
266
- ${JSON.stringify(role.default_permissions)}::jsonb,
267
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
329
+ ${JSON.stringify(role.default_permissions)}::jsonb
268
330
  )
269
331
  ON CONFLICT (id) DO NOTHING
270
332
  `);
271
333
  }
272
334
 
273
- console.log("✅ Default roles created: admin, editor, viewer");
335
+ logger.info("✅ Default roles created: admin, editor, viewer");
274
336
  }