@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.
- package/dist/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/index.es.js +1202 -369
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1200 -367
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
- package/dist/server-postgresql/src/auth/services.d.ts +43 -1
- package/dist/server-postgresql/src/connection.d.ts +25 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +7 -6
- package/src/PostgresBackendDriver.ts +13 -7
- package/src/PostgresBootstrapper.ts +27 -8
- package/src/auth/ensure-tables.ts +79 -17
- package/src/auth/services.ts +292 -23
- package/src/cli.ts +5 -0
- package/src/connection.ts +77 -0
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-schema.ts +80 -14
- package/src/schema/default-collections.ts +1 -0
- package/src/schema/doctor.ts +82 -41
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/services/EntityFetchService.ts +69 -10
- package/src/services/entityService.ts +2 -0
- package/src/services/realtimeService.ts +214 -2
- package/src/utils/drizzle-conditions.ts +74 -2
- package/src/websocket.ts +10 -2
- package/test/auth-services.test.ts +15 -28
- package/test/drizzle-conditions.test.ts +168 -0
- package/test/postgresDataDriver.test.ts +130 -1
- 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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
159
|
+
await realtimeService.startListening(directUrl);
|
|
142
160
|
} catch (err) {
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
316
|
+
logger.info(`📋 Found ${count} existing roles`);
|
|
254
317
|
return;
|
|
255
318
|
}
|
|
256
319
|
|
|
257
|
-
|
|
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
|
|
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
|
-
|
|
335
|
+
logger.info("✅ Default roles created: admin, editor, viewer");
|
|
274
336
|
}
|