@rebasepro/server-postgresql 0.4.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 CHANGED
@@ -1,106 +1,86 @@
1
- # @rebasepro/postgresql
1
+ # @rebasepro/server-postgresql
2
2
 
3
- PostgreSQL data source client for Rebase with real-time WebSocket connectivity.
4
-
5
- This package provides a complete client-side implementation for connecting Rebase applications to PostgreSQL backends, featuring real-time synchronization via WebSockets.
3
+ PostgreSQL database driver for Rebase, built on Drizzle ORM.
6
4
 
7
5
  ## Installation
8
6
 
9
7
  ```bash
10
- npm install @rebasepro/postgresql @rebasepro/core
8
+ pnpm add @rebasepro/server-postgresql
11
9
  ```
12
10
 
13
- ## Usage
11
+ ## What This Package Does
14
12
 
15
- ### Basic Setup with React Hook
13
+ Implements the Rebase `DatabaseAdapter` / `BackendBootstrapper` interfaces for PostgreSQL. It provides connection pooling, a Drizzle-based data driver, Postgres LISTEN/NOTIFY realtime, auth table management, entity history, schema generation, branching, read replicas, and WebSocket support. Plug it into `@rebasepro/server-core` via `createPostgresAdapter()` or `createPostgresBootstrapper()`.
16
14
 
17
- ```typescript
18
- import { usePostgresDataSource } from "@rebasepro/postgresql";
19
- import { Rebase } from "@rebasepro/core";
20
-
21
- function App() {
22
- const dataSource = usePostgresDataSource({
23
- baseUrl: "http://localhost:3001",
24
- websocketUrl: "ws://localhost:3001", // Optional, will be inferred from baseUrl
25
- headers: { // Optional
26
- "Authorization": "Bearer your-token"
27
- }
28
- });
29
-
30
- return (
31
- <Rebase
32
- dataSource={dataSource}
33
- collections={collections}
34
- // ... other props
35
- />
36
- );
37
- }
38
- ```
15
+ ## Key Exports
39
16
 
40
- ### Creating Data Source Directly
17
+ | Export | Description |
18
+ |--------|-------------|
19
+ | `createPostgresAdapter(config)` | Creates a `DatabaseAdapter` for use with `initializeRebaseBackend({ database: ... })`. Recommended API. |
20
+ | `createPostgresBootstrapper(config)` | Lower-level `BackendBootstrapper` factory. Used internally by the adapter. |
21
+ | `createPostgresDatabaseConnection(url, schema?, poolConfig?)` | Creates a production-grade pooled Drizzle connection. Returns `{ db, pool, connectionString }`. |
22
+ | `createDirectDatabaseConnection(url, schema?, poolConfig?)` | Non-pooled connection for LISTEN/NOTIFY and advisory locks (bypasses PgBouncer). |
23
+ | `createReadReplicaConnection(url, schema?, poolConfig?)` | Read-only connection for routing reads to replicas. |
24
+ | `PostgresBackendDriver` | The `DataDriver` implementation — CRUD, filtering, RLS, subcollections, admin SQL. |
25
+ | `RealtimeService` | Postgres LISTEN/NOTIFY-based `RealtimeProvider`. |
26
+ | `DatabasePoolManager` | Per-branch/per-tenant dynamic pool management (used with `ADMIN_CONNECTION_STRING`). |
27
+ | `PostgresCollectionRegistry` | Collection → Drizzle table registry with enum and relation tracking. |
28
+ | `BranchService` | Database branching (schema-level isolation). |
29
+ | `generateDrizzleSchema(collections)` | Generates Drizzle schema code from collection definitions. |
30
+ | `createAuthSchema(schemaName?)` | Generates Drizzle tables for the auth system (`users`, `roles`, `user_roles`). |
41
31
 
42
- ```typescript
43
- import { createPostgresDataSource } from "@rebasepro/postgresql";
32
+ ## Quick Start
44
33
 
45
- const dataSource = createPostgresDataSource({
46
- baseUrl: "http://localhost:3001",
47
- websocketUrl: "ws://localhost:3001"
34
+ ```typescript
35
+ import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
36
+ import { createPostgresAdapter } from "@rebasepro/server-postgresql";
37
+ import { initializeRebaseBackend } from "@rebasepro/server-core";
38
+ import * as schema from "./generated/schema";
39
+
40
+ // Create connection
41
+ const { db, pool } = createPostgresDatabaseConnection(
42
+ process.env.DATABASE_URL,
43
+ schema
44
+ );
45
+
46
+ // Create adapter and pass to server-core
47
+ const database = createPostgresAdapter({
48
+ connection: db,
49
+ connectionString: process.env.DATABASE_URL,
50
+ schema: { tables: schema },
48
51
  });
49
- ```
50
52
 
51
- ## Features
52
-
53
- - **Real-time Synchronization**: WebSocket-based real-time updates for collections and entities
54
- - **Automatic Reconnection**: Built-in reconnection logic with exponential backoff
55
- - **Type Safety**: Full TypeScript support with Rebase core types
56
- - **Error Handling**: Comprehensive error handling with custom error types
57
- - **Connection Management**: Connection status monitoring and queue management
58
-
59
- ## API
60
-
61
- ### Configuration
53
+ const backend = await initializeRebaseBackend({
54
+ app,
55
+ server,
56
+ database,
57
+ collections,
58
+ auth: { /* ... */ },
59
+ });
62
60
 
63
- ```typescript
64
- interface PostgresDataSourceConfig {
65
- baseUrl: string; // Backend server URL
66
- websocketUrl?: string; // WebSocket URL (optional)
67
- headers?: Record<string, string>; // Custom headers (optional)
68
- }
61
+ // Graceful shutdown
62
+ process.on("SIGTERM", async () => {
63
+ await backend.shutdown();
64
+ await pool.end();
65
+ });
69
66
  ```
70
67
 
71
- ### Methods
72
-
73
- The PostgreSQL data source implements all Rebase `DataSource` methods:
74
-
75
- - `fetchCollection<M>(props): Promise<Entity<M>[]>`
76
- - `fetchEntity<M>(props): Promise<Entity<M> | undefined>`
77
- - `saveEntity<M>(props): Promise<Entity<M>>`
78
- - `deleteEntity<M>(props): Promise<void>`
79
- - `checkUniqueField(...): Promise<boolean>`
80
- - `generateEntityId(...): string`
81
- - `countEntities<M>(props): Promise<number>`
82
- - `listenCollection<M>(props): () => void`
83
- - `listenEntity<M>(props): () => void`
84
-
85
- ## Backend Requirements
86
-
87
- This client expects a WebSocket-enabled backend that handles the following message types:
88
-
89
- - `FETCH_COLLECTION`
90
- - `FETCH_ENTITY`
91
- - `SAVE_ENTITY`
92
- - `DELETE_ENTITY`
93
- - `CHECK_UNIQUE_FIELD`
94
- - `GENERATE_ENTITY_ID`
95
- - `COUNT_ENTITIES`
96
- - `subscribe_collection`
97
- - `subscribe_entity`
98
- - `unsubscribe`
99
-
100
- ## Development
101
-
102
- This package is part of the Rebase monorepo. For development instructions, see the main repository README.
103
-
104
- ## License
105
-
106
- MIT
68
+ ## Connection Pool Defaults
69
+
70
+ | Option | Default |
71
+ |--------|---------|
72
+ | `max` | 20 |
73
+ | `idleTimeoutMillis` | 30,000 |
74
+ | `connectionTimeoutMillis` | 10,000 |
75
+ | `queryTimeout` | 30,000 |
76
+ | `statementTimeout` | 30,000 |
77
+ | `keepAlive` | true |
78
+
79
+ ## Related Packages
80
+
81
+ | Package | Role |
82
+ |---------|------|
83
+ | `@rebasepro/server-core` | Backend orchestrator that consumes this adapter |
84
+ | `@rebasepro/types` | Shared interfaces (`DatabaseAdapter`, `BackendBootstrapper`, `DataDriver`) |
85
+ | `@rebasepro/sdk-generator` | Generates typed SDKs from collections |
86
+ | `@rebasepro/common` | Default collections and shared utilities |
@@ -1,6 +1,14 @@
1
- import { AuthController, Entity, EntityCollection, User } from "@rebasepro/types";
2
- export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, entity: Entity<M> | null, targetOperation: "select" | "insert" | "update" | "delete"): boolean;
3
- export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>): boolean;
4
- export declare function canEditEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
5
- export declare function canCreateEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
6
- export declare function canDeleteEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
1
+ import { Entity, EntityCollection, User } from "@rebasepro/types";
2
+ /**
3
+ * Minimal auth context for permission checking.
4
+ * Only requires the user object avoids forcing callers to construct
5
+ * a full AuthController just to check permissions.
6
+ */
7
+ export interface AuthContext<USER extends User = User> {
8
+ user: USER | null;
9
+ }
10
+ export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, entity: Entity<M> | null, targetOperation: "select" | "insert" | "update" | "delete"): boolean;
11
+ export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>): boolean;
12
+ export declare function canEditEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
13
+ export declare function canCreateEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
14
+ export declare function canDeleteEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
package/dist/index.es.js CHANGED
@@ -3964,39 +3964,18 @@ function serializePropertyToServer(value, property) {
3964
3964
  }
3965
3965
  return value;
3966
3966
  }
3967
- case "binary":
3968
- if (typeof value === "string") {
3969
- if (value.startsWith("data:application/octet-stream;base64,")) {
3970
- const base64Data = value.split(",")[1];
3971
- if (base64Data) {
3972
- return Buffer.from(base64Data, "base64");
3973
- }
3974
- }
3975
- }
3976
- if (Buffer.isBuffer(value)) {
3977
- return value;
3978
- }
3967
+ case "binary": {
3968
+ const decoded = tryDecodeBase64DataUrl(value);
3969
+ if (decoded) return decoded;
3970
+ if (Buffer.isBuffer(value)) return value;
3979
3971
  return value;
3972
+ }
3980
3973
  case "string":
3981
- if (typeof value === "string") {
3982
- if (value.startsWith("data:application/octet-stream;base64,")) {
3983
- const base64Data = value.split(",")[1];
3984
- if (base64Data) {
3985
- return Buffer.from(base64Data, "base64");
3986
- }
3987
- }
3988
- }
3989
- return value;
3990
- default:
3991
- if (typeof value === "string") {
3992
- if (value.startsWith("data:application/octet-stream;base64,")) {
3993
- const base64Data = value.split(",")[1];
3994
- if (base64Data) {
3995
- return Buffer.from(base64Data, "base64");
3996
- }
3997
- }
3998
- }
3974
+ default: {
3975
+ const decoded = tryDecodeBase64DataUrl(value);
3976
+ if (decoded) return decoded;
3999
3977
  return value;
3978
+ }
4000
3979
  }
4001
3980
  }
4002
3981
  async function parseDataFromServer(data, collection, db, registry) {
@@ -4116,21 +4095,36 @@ async function parseDataFromServer(data, collection, db, registry) {
4116
4095
  }
4117
4096
  return result;
4118
4097
  }
4119
- function parsePropertyFromServer(value, property, collection, propertyKey) {
4120
- if (value === null || value === void 0) {
4121
- return value;
4098
+ function tryDecodeBase64DataUrl(value) {
4099
+ if (typeof value !== "string") return null;
4100
+ if (!value.startsWith("data:application/octet-stream;base64,")) return null;
4101
+ const base64Data = value.split(",")[1];
4102
+ return base64Data ? Buffer.from(base64Data, "base64") : null;
4103
+ }
4104
+ function tryResolveBuffer(value) {
4105
+ if (Buffer.isBuffer(value)) return value;
4106
+ if (typeof value === "object" && value !== null) {
4107
+ const rawVal = value;
4108
+ if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
4109
+ return Buffer.from(rawVal.data);
4110
+ }
4122
4111
  }
4112
+ return null;
4113
+ }
4114
+ function bufferToStringOrBase64(buf) {
4115
+ for (let i = 0; i < buf.length; i++) {
4116
+ const b = buf[i];
4117
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
4118
+ return `data:application/octet-stream;base64,${buf.toString("base64")}`;
4119
+ }
4120
+ }
4121
+ return buf.toString("utf8");
4122
+ }
4123
+ function parsePropertyFromServer(value, property, collection, propertyKey) {
4124
+ if (value === null || value === void 0) return value;
4123
4125
  switch (property.type) {
4124
4126
  case "binary": {
4125
- let buf = null;
4126
- if (Buffer.isBuffer(value)) {
4127
- buf = value;
4128
- } else if (typeof value === "object" && value !== null) {
4129
- const rawVal = value;
4130
- if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
4131
- buf = Buffer.from(rawVal.data);
4132
- }
4133
- }
4127
+ const buf = tryResolveBuffer(value);
4134
4128
  if (buf) {
4135
4129
  return `data:application/octet-stream;base64,${buf.toString("base64")}`;
4136
4130
  }
@@ -4138,28 +4132,9 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
4138
4132
  }
4139
4133
  case "string": {
4140
4134
  if (typeof value === "string") return value;
4141
- let isBuffer = false;
4142
- let buf = null;
4143
- if (Buffer.isBuffer(value)) {
4144
- isBuffer = true;
4145
- buf = value;
4146
- } else if (typeof value === "object" && value !== null) {
4147
- const rawVal = value;
4148
- if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
4149
- isBuffer = true;
4150
- buf = Buffer.from(rawVal.data);
4151
- }
4152
- }
4153
- if (isBuffer && buf) {
4154
- let isPrintable = true;
4155
- for (let i = 0; i < buf.length; i++) {
4156
- const b = buf[i];
4157
- if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
4158
- isPrintable = false;
4159
- break;
4160
- }
4161
- }
4162
- return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
4135
+ const buf = tryResolveBuffer(value);
4136
+ if (buf) {
4137
+ return bufferToStringOrBase64(buf);
4163
4138
  }
4164
4139
  if (typeof value === "object" && value !== null) {
4165
4140
  try {
@@ -4273,28 +4248,9 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
4273
4248
  return null;
4274
4249
  }
4275
4250
  default: {
4276
- let isBuffer = false;
4277
- let buf = null;
4278
- if (Buffer.isBuffer(value)) {
4279
- isBuffer = true;
4280
- buf = value;
4281
- } else if (typeof value === "object" && value !== null) {
4282
- const rawVal = value;
4283
- if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
4284
- isBuffer = true;
4285
- buf = Buffer.from(rawVal.data);
4286
- }
4287
- }
4288
- if (isBuffer && buf) {
4289
- let isPrintable = true;
4290
- for (let i = 0; i < buf.length; i++) {
4291
- const b = buf[i];
4292
- if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
4293
- isPrintable = false;
4294
- break;
4295
- }
4296
- }
4297
- return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
4251
+ const buf = tryResolveBuffer(value);
4252
+ if (buf) {
4253
+ return bufferToStringOrBase64(buf);
4298
4254
  }
4299
4255
  return value;
4300
4256
  }
@@ -6487,12 +6443,12 @@ class EntityPersistService {
6487
6443
  */
6488
6444
  extractCauseMessage(error) {
6489
6445
  if (!error || typeof error !== "object") return null;
6490
- const err = error;
6491
- if (err.cause && typeof err.cause === "object") {
6492
- const deeper = this.extractCauseMessage(err.cause);
6446
+ if (!(error instanceof Error)) return null;
6447
+ if (error.cause && typeof error.cause === "object") {
6448
+ const deeper = this.extractCauseMessage(error.cause);
6493
6449
  if (deeper) return deeper;
6494
- if (err.cause instanceof Error && err.cause.message) {
6495
- return err.cause.message;
6450
+ if (error.cause instanceof Error && error.cause.message) {
6451
+ return error.cause.message;
6496
6452
  }
6497
6453
  }
6498
6454
  return null;
@@ -6513,12 +6469,17 @@ class EntityPersistService {
6513
6469
  */
6514
6470
  extractPgError(error) {
6515
6471
  if (!error || typeof error !== "object") return null;
6516
- const err = error;
6517
- if (err.code && /^[0-9A-Z]{5}$/.test(err.code)) {
6518
- return err;
6472
+ if (!(error instanceof Error)) {
6473
+ if ("cause" in error && error.cause && typeof error.cause === "object") {
6474
+ return this.extractPgError(error.cause);
6475
+ }
6476
+ return null;
6477
+ }
6478
+ if ("code" in error && typeof error.code === "string" && /^[0-9A-Z]{5}$/.test(error.code)) {
6479
+ return error;
6519
6480
  }
6520
- if (err.cause && typeof err.cause === "object") {
6521
- return this.extractPgError(err.cause);
6481
+ if (error.cause && typeof error.cause === "object") {
6482
+ return this.extractPgError(error.cause);
6522
6483
  }
6523
6484
  return null;
6524
6485
  }
@@ -7633,7 +7594,7 @@ class AuthenticatedPostgresBackendDriver {
7633
7594
  return this.withTransaction((delegate) => delegate.deleteEntity(props));
7634
7595
  }
7635
7596
  async deleteAll(path2) {
7636
- return this.delegate.deleteAll(path2);
7597
+ return this.withTransaction((delegate) => delegate.deleteAll(path2));
7637
7598
  }
7638
7599
  async checkUniqueField(path2, name, value, entityId, collection) {
7639
7600
  return this.withTransaction((delegate) => delegate.checkUniqueField(path2, name, value, entityId, collection));
@@ -7712,6 +7673,7 @@ class DatabasePoolManager {
7712
7673
  }
7713
7674
  await Promise.all(promises2);
7714
7675
  this.pools.clear();
7676
+ this.drizzleInstances.clear();
7715
7677
  }
7716
7678
  }
7717
7679
  function createAuthSchema(usersSchemaName = "rebase") {
@@ -9605,20 +9567,19 @@ class RealtimeService extends EventEmitter {
9605
9567
  }
9606
9568
  }
9607
9569
  const PostgresRealtimeProvider = RealtimeService;
9608
- const clientSessions = /* @__PURE__ */ new Map();
9609
9570
  const WS_RATE_LIMIT = 2e3;
9610
9571
  const WS_RATE_WINDOW_MS = 6e4;
9611
9572
  const ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set(["EXECUTE_SQL", "FETCH_DATABASES", "FETCH_ROLES", "FETCH_UNMAPPED_TABLES", "FETCH_TABLE_METADATA", "FETCH_CURRENT_DATABASE", "CREATE_BRANCH", "DELETE_BRANCH", "LIST_BRANCHES"]);
9612
9573
  function extractErrorMessage(error) {
9613
9574
  if (!error) return "Unknown error";
9614
- if (typeof error === "object") {
9615
- const err = error;
9616
- if (err.cause) {
9617
- return extractErrorMessage(err.cause);
9618
- }
9619
- if (typeof err.message === "string") {
9620
- return err.message;
9575
+ if (error instanceof Error) {
9576
+ if ("cause" in error && error.cause) {
9577
+ return extractErrorMessage(error.cause);
9621
9578
  }
9579
+ return error.message;
9580
+ }
9581
+ if (typeof error === "object" && "message" in error && typeof error.message === "string") {
9582
+ return error.message;
9622
9583
  }
9623
9584
  return String(error);
9624
9585
  }
@@ -9626,14 +9587,10 @@ function isAdminSession(session) {
9626
9587
  if (!session?.user) return false;
9627
9588
  if (session.user.isAdmin) return true;
9628
9589
  if (!session.user.roles) return false;
9629
- return session.user.roles.some((r) => {
9630
- if (typeof r === "string") return r === "admin";
9631
- if (r && typeof r === "object" && "isAdmin" in r) return r.isAdmin;
9632
- if (r && typeof r === "object" && "id" in r) return r.id === "admin";
9633
- return false;
9634
- });
9590
+ return session.user.roles.some((r) => r === "admin");
9635
9591
  }
9636
9592
  function createPostgresWebSocket(server, realtimeService, driver, authConfig, authAdapter) {
9593
+ const clientSessions = /* @__PURE__ */ new Map();
9637
9594
  const isProduction = process.env.NODE_ENV === "production";
9638
9595
  const wsDebug = (...args) => {
9639
9596
  if (!isProduction) console.debug(...args);
@@ -9772,13 +9729,23 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
9772
9729
  }
9773
9730
  const getScopedDelegate = async () => {
9774
9731
  const session = clientSessions.get(clientId);
9775
- if ("withAuth" in driver && typeof driver.withAuth === "function") {
9732
+ if (typeof driver.withAuth === "function") {
9776
9733
  try {
9777
9734
  const userForAuth = session?.user ? {
9778
9735
  uid: session.user.userId,
9736
+ displayName: null,
9737
+ email: null,
9738
+ photoURL: null,
9739
+ providerId: "websocket",
9740
+ isAnonymous: false,
9779
9741
  roles: session.user.roles ?? []
9780
9742
  } : {
9781
9743
  uid: "anon",
9744
+ displayName: null,
9745
+ email: null,
9746
+ photoURL: null,
9747
+ providerId: "websocket",
9748
+ isAnonymous: true,
9782
9749
  roles: ["anon"]
9783
9750
  };
9784
9751
  return await driver.withAuth(userForAuth);