@rebasepro/server-postgresql 0.0.1-canary.dbf160a → 0.0.1-canary.e17585f

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 (74) hide show
  1. package/dist/index.es.js +683 -1362
  2. package/dist/index.es.js.map +1 -1
  3. package/dist/index.umd.js +614 -1293
  4. package/dist/index.umd.js.map +1 -1
  5. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  6. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +8 -1
  7. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  8. package/dist/server-postgresql/src/index.d.ts +1 -0
  9. package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
  10. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
  11. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
  12. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  13. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  14. package/dist/types/src/controllers/auth.d.ts +8 -2
  15. package/dist/types/src/controllers/client.d.ts +13 -0
  16. package/dist/types/src/controllers/collection_registry.d.ts +2 -1
  17. package/dist/types/src/controllers/data_driver.d.ts +36 -1
  18. package/dist/types/src/controllers/navigation.d.ts +18 -6
  19. package/dist/types/src/controllers/registry.d.ts +9 -1
  20. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
  21. package/dist/types/src/rebase_context.d.ts +17 -0
  22. package/dist/types/src/types/auth_adapter.d.ts +354 -0
  23. package/dist/types/src/types/backend_hooks.d.ts +187 -0
  24. package/dist/types/src/types/collections.d.ts +75 -11
  25. package/dist/types/src/types/component_ref.d.ts +47 -0
  26. package/dist/types/src/types/cron.d.ts +1 -1
  27. package/dist/types/src/types/database_adapter.d.ts +90 -0
  28. package/dist/types/src/types/entity_views.d.ts +6 -7
  29. package/dist/types/src/types/formex.d.ts +40 -0
  30. package/dist/types/src/types/index.d.ts +5 -0
  31. package/dist/types/src/types/plugins.d.ts +6 -3
  32. package/dist/types/src/types/properties.d.ts +72 -88
  33. package/dist/types/src/types/slots.d.ts +20 -10
  34. package/dist/types/src/types/translations.d.ts +12 -0
  35. package/package.json +5 -5
  36. package/src/PostgresAdapter.ts +52 -0
  37. package/src/PostgresBackendDriver.ts +49 -7
  38. package/src/PostgresBootstrapper.ts +4 -7
  39. package/src/auth/ensure-tables.ts +3 -121
  40. package/src/cli.ts +10 -2
  41. package/src/data-transformer.ts +84 -1
  42. package/src/index.ts +1 -0
  43. package/src/schema/doctor.ts +14 -2
  44. package/src/schema/generate-drizzle-schema-logic.ts +59 -30
  45. package/src/schema/introspect-db-inference.ts +238 -0
  46. package/src/schema/introspect-db-logic.ts +365 -61
  47. package/src/schema/introspect-db.ts +66 -23
  48. package/src/services/EntityFetchService.ts +16 -0
  49. package/src/services/EntityPersistService.ts +95 -13
  50. package/src/services/realtimeService.ts +35 -0
  51. package/src/utils/drizzle-conditions.ts +6 -0
  52. package/src/websocket.ts +60 -11
  53. package/test/generate-drizzle-schema.test.ts +342 -0
  54. package/test/introspect-db-generation.test.ts +32 -10
  55. package/test/property-ordering.test.ts +395 -0
  56. package/test/relations.test.ts +4 -4
  57. package/jest-all.log +0 -3128
  58. package/jest.log +0 -49
  59. package/scratch.ts +0 -41
  60. package/test-drizzle-bug.ts +0 -18
  61. package/test-drizzle-out/0000_cultured_freak.sql +0 -7
  62. package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
  63. package/test-drizzle-out/meta/0000_snapshot.json +0 -55
  64. package/test-drizzle-out/meta/0001_snapshot.json +0 -63
  65. package/test-drizzle-out/meta/_journal.json +0 -20
  66. package/test-drizzle-prompt.sh +0 -2
  67. package/test-policy-prompt.sh +0 -3
  68. package/test-programmatic.ts +0 -30
  69. package/test-programmatic2.ts +0 -59
  70. package/test-schema-no-policies.ts +0 -12
  71. package/test_drizzle_mock.js +0 -3
  72. package/test_find_changed.mjs +0 -32
  73. package/test_hash.js +0 -14
  74. package/test_output.txt +0 -3145
@@ -51,6 +51,8 @@ export interface RebaseTranslations {
51
51
  all_entries_loaded: string;
52
52
  create_your_first_entry: string;
53
53
  no_results_filter_sort: string;
54
+ /** Shown when a text search yields no results. Supports `{{search}}` interpolation. */
55
+ no_results_search?: string;
54
56
  add: string;
55
57
  remove: string;
56
58
  copy_id: string;
@@ -390,6 +392,12 @@ export interface RebaseTranslations {
390
392
  submit: string;
391
393
  no_filterable_properties: string;
392
394
  apply_filters: string;
395
+ /** Label shown on the filter presets dropdown trigger */
396
+ filter_presets?: string;
397
+ /** Tooltip shown when hovering over a preset entry */
398
+ filter_preset_apply?: string;
399
+ /** Shown when a preset is active, with {{label}} interpolation */
400
+ filter_preset_active?: string;
393
401
  list: string;
394
402
  table_view_mode: string;
395
403
  cards: string;
@@ -461,6 +469,10 @@ export interface RebaseTranslations {
461
469
  reset_password_success?: string;
462
470
  reset_password_confirmation?: string;
463
471
  error_resetting_password?: string;
472
+ /** Permission-denied empty states */
473
+ no_permission_to_view_users?: string;
474
+ no_permission_to_view_roles?: string;
475
+ no_permission_description?: string;
464
476
  /** Editor table-bubble */
465
477
  add_row_before: string;
466
478
  add_row_after: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgresql",
3
3
  "type": "module",
4
- "version": "0.0.1-canary.dbf160a",
4
+ "version": "0.0.1-canary.e17585f",
5
5
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -64,10 +64,10 @@
64
64
  "drizzle-orm": "^0.44.4",
65
65
  "execa": "^4.1.0",
66
66
  "pg": "^8.11.3",
67
- "@rebasepro/server-core": "0.0.1-canary.dbf160a",
68
- "@rebasepro/common": "0.0.1-canary.dbf160a",
69
- "@rebasepro/types": "0.0.1-canary.dbf160a",
70
- "@rebasepro/utils": "0.0.1-canary.dbf160a"
67
+ "@rebasepro/common": "0.0.1-canary.e17585f",
68
+ "@rebasepro/server-core": "0.0.1-canary.e17585f",
69
+ "@rebasepro/utils": "0.0.1-canary.e17585f",
70
+ "@rebasepro/types": "0.0.1-canary.e17585f"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/jest": "^29.5.14",
@@ -0,0 +1,52 @@
1
+ import { DatabaseAdapter, InitializedDriver, RealtimeProvider, DataDriver, DatabaseAdmin } from "@rebasepro/types";
2
+ import { createPostgresBootstrapper } from "./PostgresBootstrapper";
3
+ // @ts-ignore
4
+ import type { PostgresDriverConfig } from "@rebasepro/server-core";
5
+
6
+ /**
7
+ * Creates a Postgres database adapter for Rebase.
8
+ */
9
+ export function createPostgresAdapter(pgConfig: PostgresDriverConfig): DatabaseAdapter {
10
+ const bootstrapper = createPostgresBootstrapper(pgConfig);
11
+
12
+ return {
13
+ type: bootstrapper.type,
14
+
15
+ async initializeDriver(config) {
16
+ return bootstrapper.initializeDriver(config);
17
+ },
18
+
19
+ async initializeRealtime(driverResult) {
20
+ if (bootstrapper.initializeRealtime) {
21
+ return bootstrapper.initializeRealtime({}, driverResult);
22
+ }
23
+ return undefined;
24
+ },
25
+
26
+ async initializeHistory(config, driverResult) {
27
+ if (bootstrapper.initializeHistory) {
28
+ return bootstrapper.initializeHistory(config, driverResult) as any;
29
+ }
30
+ return undefined;
31
+ },
32
+
33
+ initializeWebsockets(server, realtimeService, driver, config) {
34
+ if (bootstrapper.initializeWebsockets) {
35
+ return bootstrapper.initializeWebsockets(server, realtimeService, driver, config);
36
+ }
37
+ },
38
+
39
+ getAdmin(driverResult) {
40
+ if (bootstrapper.getAdmin) {
41
+ return bootstrapper.getAdmin(driverResult);
42
+ }
43
+ return undefined;
44
+ },
45
+
46
+ mountRoutes(app, basePath, driverResult) {
47
+ if (bootstrapper.mountRoutes) {
48
+ bootstrapper.mountRoutes(app, basePath, driverResult);
49
+ }
50
+ }
51
+ };
52
+ }
@@ -4,9 +4,9 @@ import { BranchService } from "./services/BranchService";
4
4
  import { RealtimeService } from "./services/realtimeService";
5
5
  import { DatabasePoolManager } from "./databasePoolManager";
6
6
  import { DrizzleClient } from "./interfaces";
7
- import { User } from "@rebasepro/types";
7
+ import { User, RebaseClient } from "@rebasepro/types";
8
8
  import { sql as drizzleSql } from "drizzle-orm";
9
- import { buildPropertyCallbacks } from "@rebasepro/common";
9
+ import { buildPropertyCallbacks, updateDateAutoValues } from "@rebasepro/common";
10
10
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
11
11
  import {
12
12
  DataDriver,
@@ -44,6 +44,7 @@ export class PostgresBackendDriver implements DataDriver {
44
44
  public branchService?: BranchService;
45
45
  public user?: User;
46
46
  public data: RebaseData;
47
+ public client?: RebaseClient;
47
48
 
48
49
  /**
49
50
  * When true, realtime notifications are deferred until after the
@@ -101,6 +102,15 @@ export class PostgresBackendDriver implements DataDriver {
101
102
  };
102
103
  }
103
104
 
105
+ /**
106
+ * REST-optimised fetch service (include-aware eager-loading).
107
+ * Delegates to the underlying EntityFetchService which already
108
+ * implements the matching method signatures.
109
+ */
110
+ get restFetchService() {
111
+ return this.entityService.getFetchService();
112
+ }
113
+
104
114
 
105
115
  private resolveCollectionCallbacks<M extends Record<string, unknown>>(collection: EntityCollection<M> | undefined, path: string) {
106
116
  if (!collection && !path) return { collection: undefined,
@@ -154,7 +164,8 @@ propertyCallbacks: undefined };
154
164
  const contextForCallback = {
155
165
  user: this.user,
156
166
  driver: this,
157
- data: this.data
167
+ data: this.data,
168
+ client: this.client
158
169
  } as unknown as RebaseCallContext; // Backend context
159
170
  return Promise.all(entities.map(async (entity) => {
160
171
  let fetched = entity;
@@ -263,7 +274,8 @@ propertyCallbacks: undefined };
263
274
  const contextForCallback = {
264
275
  user: this.user,
265
276
  driver: this,
266
- data: this.data
277
+ data: this.data,
278
+ client: this.client
267
279
  } as unknown as RebaseCallContext; // Backend context
268
280
  if (callbacks?.afterRead) {
269
281
  entity = await callbacks.afterRead({
@@ -345,7 +357,8 @@ propertyCallbacks: undefined };
345
357
  const contextForCallback = {
346
358
  user: this.user,
347
359
  driver: this,
348
- data: this.data
360
+ data: this.data,
361
+ client: this.client
349
362
  } as unknown as RebaseCallContext;
350
363
 
351
364
  // Fetch previous values for callbacks AND history recording
@@ -386,6 +399,17 @@ propertyCallbacks: undefined };
386
399
 
387
400
  }
388
401
 
402
+ // Apply autoValue timestamps (on_create / on_update) at the application layer.
403
+ // This handles updated_at fields for all writes that flow through the Rebase backend.
404
+ if (resolvedCollection?.properties) {
405
+ updatedValues = updateDateAutoValues({
406
+ inputValues: updatedValues,
407
+ properties: resolvedCollection.properties,
408
+ status: status ?? "new",
409
+ timestampNowValue: new Date()
410
+ });
411
+ }
412
+
389
413
  try {
390
414
  let savedEntity = await this.entityService.saveEntity<M>(
391
415
  path,
@@ -508,7 +532,8 @@ propertyCallbacks: undefined };
508
532
  const contextForCallback = {
509
533
  user: this.user,
510
534
  driver: this,
511
- data: this.data
535
+ data: this.data,
536
+ client: this.client
512
537
  } as unknown as RebaseCallContext;
513
538
 
514
539
  if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
@@ -639,7 +664,23 @@ searchString }
639
664
  const targetDb = this.getTargetDb(options?.database);
640
665
 
641
666
  try {
642
- if (options?.role) {
667
+ // Determine if we actually need to switch roles.
668
+ // Skip SET LOCAL ROLE when the requested role matches the current session role,
669
+ // as it's a no-op that can fail on managed Postgres setups where the connection
670
+ // user doesn't have permission to SET ROLE.
671
+ let needsRoleSwitch = false;
672
+ if (options?.role && process.env.DISABLE_DB_ROLE_SWITCHING !== "true") {
673
+ try {
674
+ const currentRoleResult = await targetDb.execute(drizzleSql.raw("SELECT current_user AS role"));
675
+ const currentRole = (currentRoleResult.rows?.[0] as Record<string, unknown>)?.role as string | undefined;
676
+ needsRoleSwitch = !!currentRole && currentRole !== options.role;
677
+ } catch {
678
+ // If we can't determine the current role, attempt the switch anyway
679
+ needsRoleSwitch = true;
680
+ }
681
+ }
682
+
683
+ if (needsRoleSwitch && options?.role) {
643
684
  const safeRole = options.role.replace(/"/g, '""');
644
685
  return await targetDb.transaction(async (tx) => {
645
686
  await tx.execute(drizzleSql.raw(`SET LOCAL ROLE "${safeRole}"`));
@@ -931,6 +972,7 @@ roles: userRoles })}, true)
931
972
  txDelegate.entityService = txEntityService;
932
973
  txDelegate._deferNotifications = true;
933
974
  txDelegate._pendingNotifications = pendingNotifications;
975
+ txDelegate.client = this.delegate.client;
934
976
 
935
977
  return await operation(txDelegate);
936
978
  });
@@ -2,11 +2,6 @@
2
2
  * PostgresBootstrapper
3
3
  *
4
4
  * Implements the `BackendBootstrapper` interface for PostgreSQL.
5
- * Encapsulates all Postgres-specific initialization logic that was previously
6
- * hardcoded inside `initializeRebaseBackend()`.
7
- *
8
- * Third-party drivers (MongoDB, MySQL, etc.) can implement their own
9
- * bootstrapper following this pattern and pass it to the coordinator.
10
5
  */
11
6
 
12
7
  import { getTableName, isTable, Relations, sql } from "drizzle-orm";
@@ -19,6 +14,7 @@ import {
19
14
  DatabaseAdmin,
20
15
  RealtimeProvider,
21
16
  type DataDriver,
17
+ type AuthAdapter,
22
18
  EntityCollection
23
19
  } from "@rebasepro/types";
24
20
  import { PostgresBackendDriver } from "./PostgresBackendDriver";
@@ -218,13 +214,14 @@ authRepository };
218
214
  // Currently Postgres doesn't need additional routes beyond what the coordinator mounts.
219
215
  },
220
216
 
221
- async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown): Promise<void> {
217
+ async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, adapter?: unknown): Promise<void> {
222
218
  const { createPostgresWebSocket } = await import("./websocket");
223
219
  createPostgresWebSocket(
224
220
  server as import("http").Server,
225
221
  realtimeService as RealtimeService,
226
222
  driver as PostgresBackendDriver,
227
- config as AuthConfig
223
+ config as AuthConfig,
224
+ adapter as AuthAdapter | undefined
228
225
  );
229
226
  }
230
227
  };
@@ -62,6 +62,9 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
62
62
  password_hash TEXT,
63
63
  display_name TEXT,
64
64
  photo_url TEXT,
65
+ email_verified BOOLEAN DEFAULT FALSE,
66
+ email_verification_token TEXT,
67
+ email_verification_sent_at TIMESTAMP WITH TIME ZONE,
65
68
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
66
69
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
67
70
  )
@@ -181,9 +184,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
181
184
  )
182
185
  `);
183
186
 
184
- // Apply any schema alterations for existing databases
185
- await applyInternalMigrations(db);
186
-
187
187
  // Create the `auth` schema with Supabase-style helper functions for RLS.
188
188
  // auth.uid() → returns the current user's ID (reads app.user_id)
189
189
  // auth.jwt() → returns the full JWT claims as JSONB (reads app.jwt)
@@ -261,121 +261,3 @@ async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
261
261
  console.log("✅ Default roles created: admin, editor, viewer");
262
262
  }
263
263
 
264
- /**
265
- * Apply idempotent alterations for internal Rebase tables.
266
- * This runs after CREATE TABLE IF NOT EXISTS to ensure existing
267
- * databases get new columns without needing external Drizzle migrations.
268
- */
269
- async function applyInternalMigrations(db: NodePgDatabase): Promise<void> {
270
- try {
271
- // Users Table Migrations
272
- await db.execute(sql`
273
- ALTER TABLE rebase.users
274
- ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
275
- ADD COLUMN IF NOT EXISTS email_verification_token TEXT,
276
- ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
277
- `);
278
-
279
- // Migrate Old OAuth Data to user_identities table
280
-
281
- // 1. Check if legacy columns exist
282
- const columnsCheck = await db.execute(sql`
283
- SELECT column_name
284
- FROM information_schema.columns
285
- WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
286
- `);
287
- const existingColumns = columnsCheck.rows.map(r => r.column_name);
288
-
289
- if (existingColumns.includes("google_id")) {
290
- // Migrate google users
291
- await db.execute(sql`
292
- INSERT INTO rebase.user_identities (user_id, provider, provider_id)
293
- SELECT id, 'google', google_id
294
- FROM rebase.users
295
- WHERE google_id IS NOT NULL
296
- ON CONFLICT (provider, provider_id) DO NOTHING
297
- `);
298
- }
299
-
300
- if (existingColumns.includes("linkedin_id")) {
301
- // Migrate linkedin users
302
- await db.execute(sql`
303
- INSERT INTO rebase.user_identities (user_id, provider, provider_id)
304
- SELECT id, 'linkedin', linkedin_id
305
- FROM rebase.users
306
- WHERE linkedin_id IS NOT NULL
307
- ON CONFLICT (provider, provider_id) DO NOTHING
308
- `);
309
- }
310
-
311
- // Now drop legacy columns safely if they exist
312
- if (existingColumns.length > 0) {
313
- await db.execute(sql`
314
- ALTER TABLE rebase.users
315
- DROP COLUMN IF EXISTS provider,
316
- DROP COLUMN IF EXISTS google_id,
317
- DROP COLUMN IF EXISTS linkedin_id
318
- `);
319
-
320
- // Drop legacy indexes
321
- await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
322
- await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
323
-
324
- console.log("✅ Migrated to user_identities and dropped legacy columns.");
325
- }
326
-
327
- // Roles Table Migrations
328
- await db.execute(sql`
329
- ALTER TABLE rebase.roles
330
- ADD COLUMN IF NOT EXISTS collection_permissions JSONB
331
- `);
332
-
333
- // Refresh Tokens Table Migrations
334
- await db.execute(sql`
335
- ALTER TABLE rebase.refresh_tokens
336
- ADD COLUMN IF NOT EXISTS user_agent TEXT,
337
- ADD COLUMN IF NOT EXISTS ip_address TEXT
338
- `);
339
-
340
- const constraintCheck = await db.execute(sql`
341
- SELECT 1 FROM information_schema.table_constraints
342
- WHERE constraint_name = 'unique_device_session'
343
- AND table_schema = 'rebase'
344
- AND table_name = 'refresh_tokens'
345
- `);
346
-
347
- if (constraintCheck.rows.length === 0) {
348
- try {
349
- await db.execute(sql`
350
- ALTER TABLE rebase.refresh_tokens
351
- ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
352
- `);
353
- console.log("✅ Added unique_device_session constraint");
354
- } catch (e: unknown) {
355
- const errorMessage = e instanceof Error ? e.message : String(e);
356
- if (errorMessage.includes("could not create unique index")) {
357
- console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
358
- await db.execute(sql`
359
- DELETE FROM rebase.refresh_tokens a
360
- USING rebase.refresh_tokens b
361
- WHERE a.user_id = b.user_id
362
- AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
363
- AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
364
- AND a.created_at < b.created_at
365
- `);
366
- await db.execute(sql`
367
- ALTER TABLE rebase.refresh_tokens
368
- ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
369
- `).catch((retryErr: unknown) => {
370
- const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
371
- console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
372
- });
373
- } else {
374
- console.error("Constraint migration issue:", errorMessage);
375
- }
376
- }
377
- }
378
- } catch (error) {
379
- console.error("❌ Failed to run internal migrations:", error);
380
- }
381
- }
package/src/cli.ts CHANGED
@@ -43,7 +43,7 @@ export async function runPluginCommand(args: string[]) {
43
43
  }
44
44
 
45
45
  async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
46
- const VALID_ACTIONS = ["push", "pull", "generate", "migrate", "studio", "branch"];
46
+ const VALID_ACTIONS = ["push", "generate", "migrate", "studio", "branch"];
47
47
  if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
48
48
  console.error(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
49
49
  process.exit(1);
@@ -68,6 +68,12 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
68
68
  console.log("");
69
69
  console.log(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
70
70
  console.log("");
71
+ } else if (subcommand === "pull") {
72
+ console.log("");
73
+ console.log(chalk.yellow(" ⚠ \"rebase db pull\" has been removed."));
74
+ console.log(chalk.gray(" Use \"rebase schema introspect\" instead."));
75
+ console.log("");
76
+ process.exit(1);
71
77
  } else {
72
78
  console.log("");
73
79
  console.log(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
@@ -538,9 +544,11 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
538
544
  const argsList = arg(
539
545
  {
540
546
  "--output": String,
547
+ "--collections": String,
541
548
  "--force": Boolean,
542
549
  "--schema": String,
543
550
  "-o": "--output",
551
+ "-c": "--collections",
544
552
  "-f": "--force"
545
553
  },
546
554
  {
@@ -561,7 +569,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
561
569
  process.exit(1);
562
570
  }
563
571
 
564
- const outputPath = argsList["--output"] || path.join("config", "collections");
572
+ const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
565
573
 
566
574
  console.log("");
567
575
  console.log(chalk.bold(" 🔍 Rebase Schema Introspector"));
@@ -258,7 +258,26 @@ export function serializePropertyToServer(value: unknown, property: Property): u
258
258
  }
259
259
  return value;
260
260
 
261
+ case "string":
262
+ if (typeof value === "string") {
263
+ if (value.startsWith("data:application/octet-stream;base64,")) {
264
+ const base64Data = value.split(",")[1];
265
+ if (base64Data) {
266
+ return Buffer.from(base64Data, "base64");
267
+ }
268
+ }
269
+ }
270
+ return value;
271
+
261
272
  default:
273
+ if (typeof value === "string") {
274
+ if (value.startsWith("data:application/octet-stream;base64,")) {
275
+ const base64Data = value.split(",")[1];
276
+ if (base64Data) {
277
+ return Buffer.from(base64Data, "base64");
278
+ }
279
+ }
280
+ }
262
281
  return value;
263
282
  }
264
283
  }
@@ -447,6 +466,45 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
447
466
  }
448
467
 
449
468
  switch (property.type) {
469
+ case "string": {
470
+ if (typeof value === "string") return value;
471
+
472
+ // Handle Buffer objects (e.g. from PostgreSQL bytea columns)
473
+ let isBuffer = false;
474
+ let buf: Buffer | null = null;
475
+
476
+ if (Buffer.isBuffer(value)) {
477
+ isBuffer = true;
478
+ buf = value;
479
+ } else if (typeof value === "object" && value !== null && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
480
+ isBuffer = true;
481
+ buf = Buffer.from((value as any).data);
482
+ }
483
+
484
+ if (isBuffer && buf) {
485
+ // Heuristic: if all bytes are printable ASCII, return utf8, else base64
486
+ let isPrintable = true;
487
+ for (let i = 0; i < buf.length; i++) {
488
+ const b = buf[i];
489
+ // Allow standard printable ASCII + common whitespace (\r, \n, \t)
490
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
491
+ isPrintable = false;
492
+ break;
493
+ }
494
+ }
495
+ return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
496
+ }
497
+
498
+ if (typeof value === "object" && value !== null) {
499
+ try {
500
+ return JSON.stringify(value);
501
+ } catch {
502
+ return String(value);
503
+ }
504
+ }
505
+ return String(value);
506
+ }
507
+
450
508
  case "relation":
451
509
  // Transform ID back to relation object with type information
452
510
  if (typeof value === "string" || typeof value === "number") {
@@ -538,8 +596,33 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
538
596
  return null;
539
597
  }
540
598
 
541
- default:
599
+ default: {
600
+ // Fallback for buffers in case they are mapped to something other than string
601
+ let isBuffer = false;
602
+ let buf: Buffer | null = null;
603
+
604
+ if (Buffer.isBuffer(value)) {
605
+ isBuffer = true;
606
+ buf = value;
607
+ } else if (typeof value === "object" && value !== null && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
608
+ isBuffer = true;
609
+ buf = Buffer.from((value as any).data);
610
+ }
611
+
612
+ if (isBuffer && buf) {
613
+ let isPrintable = true;
614
+ for (let i = 0; i < buf.length; i++) {
615
+ const b = buf[i];
616
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
617
+ isPrintable = false;
618
+ break;
619
+ }
620
+ }
621
+ return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
622
+ }
623
+
542
624
  return value;
625
+ }
543
626
  }
544
627
  }
545
628
 
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./websocket";
11
11
  export * from "./collections/PostgresCollectionRegistry";
12
12
  export * from "./services/BranchService";
13
13
  export * from "./PostgresBootstrapper";
14
+ export * from "./PostgresAdapter";
@@ -17,6 +17,18 @@ import { generateSchema } from "./generate-drizzle-schema-logic";
17
17
  import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
18
18
  import { toSnakeCase } from "@rebasepro/utils";
19
19
 
20
+ /**
21
+ * Resolve the SQL column name for a property.
22
+ * Uses the explicit `columnName` when set (e.g. from introspection),
23
+ * falling back to `toSnakeCase(propName)` for manually-authored collections.
24
+ */
25
+ const resolveColumnName = (propName: string, prop?: Property | null): string => {
26
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") {
27
+ return prop.columnName;
28
+ }
29
+ return toSnakeCase(propName);
30
+ };
31
+
20
32
  // ── Types ────────────────────────────────────────────────────────────────
21
33
 
22
34
  export type IssueSeverity = "error" | "warning" | "info";
@@ -316,7 +328,7 @@ export async function checkCollectionsVsDatabase(
316
328
  const resolvedRelations = resolveCollectionRelations(collection);
317
329
  const relation = findRelation(resolvedRelations, (prop as RelationProperty).relationName ?? propName);
318
330
  if (relation?.direction === "owning" && relation.cardinality === "one" && relation.localKey) {
319
- const fkColName = toSnakeCase(relation.localKey);
331
+ const fkColName = relation.localKey;
320
332
  if (!dbColumnMap.has(fkColName)) {
321
333
  issues.push({
322
334
  severity: "error",
@@ -349,7 +361,7 @@ export async function checkCollectionsVsDatabase(
349
361
  continue;
350
362
  }
351
363
 
352
- const colName = toSnakeCase(propName);
364
+ const colName = resolveColumnName(propName, prop);
353
365
 
354
366
  // Skip system columns — they're handled automatically
355
367
  if (systemColumns.has(colName)) continue;