@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.
- package/dist/index.es.js +683 -1362
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +614 -1293
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +8 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/collection_registry.d.ts +2 -1
- package/dist/types/src/controllers/data_driver.d.ts +36 -1
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/auth_adapter.d.ts +354 -0
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +75 -11
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/database_adapter.d.ts +90 -0
- package/dist/types/src/types/entity_views.d.ts +6 -7
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +72 -88
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +12 -0
- package/package.json +5 -5
- package/src/PostgresAdapter.ts +52 -0
- package/src/PostgresBackendDriver.ts +49 -7
- package/src/PostgresBootstrapper.ts +4 -7
- package/src/auth/ensure-tables.ts +3 -121
- package/src/cli.ts +10 -2
- package/src/data-transformer.ts +84 -1
- package/src/index.ts +1 -0
- package/src/schema/doctor.ts +14 -2
- package/src/schema/generate-drizzle-schema-logic.ts +59 -30
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +365 -61
- package/src/schema/introspect-db.ts +66 -23
- package/src/services/EntityFetchService.ts +16 -0
- package/src/services/EntityPersistService.ts +95 -13
- package/src/services/realtimeService.ts +35 -0
- package/src/utils/drizzle-conditions.ts +6 -0
- package/src/websocket.ts +60 -11
- package/test/generate-drizzle-schema.test.ts +342 -0
- package/test/introspect-db-generation.test.ts +32 -10
- package/test/property-ordering.test.ts +395 -0
- package/test/relations.test.ts +4 -4
- package/jest-all.log +0 -3128
- package/jest.log +0 -49
- package/scratch.ts +0 -41
- package/test-drizzle-bug.ts +0 -18
- package/test-drizzle-out/0000_cultured_freak.sql +0 -7
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
- package/test-drizzle-out/meta/0000_snapshot.json +0 -55
- package/test-drizzle-out/meta/0001_snapshot.json +0 -63
- package/test-drizzle-out/meta/_journal.json +0 -20
- package/test-drizzle-prompt.sh +0 -2
- package/test-policy-prompt.sh +0 -3
- package/test-programmatic.ts +0 -30
- package/test-programmatic2.ts +0 -59
- package/test-schema-no-policies.ts +0 -12
- package/test_drizzle_mock.js +0 -3
- package/test_find_changed.mjs +0 -32
- package/test_hash.js +0 -14
- 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.
|
|
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/
|
|
68
|
-
"@rebasepro/
|
|
69
|
-
"@rebasepro/
|
|
70
|
-
"@rebasepro/
|
|
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
|
|
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", "
|
|
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"));
|
package/src/data-transformer.ts
CHANGED
|
@@ -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
package/src/schema/doctor.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
364
|
+
const colName = resolveColumnName(propName, prop);
|
|
353
365
|
|
|
354
366
|
// Skip system columns — they're handled automatically
|
|
355
367
|
if (systemColumns.has(colName)) continue;
|