@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e
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/LICENSE +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +36 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +10635 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +10643 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +188 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +7 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/index.d.ts +17 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +93 -0
- package/src/PostgresBackendDriver.ts +1024 -0
- package/src/PostgresBootstrapper.ts +232 -0
- package/src/auth/ensure-tables.ts +309 -0
- package/src/auth/services.ts +740 -0
- package/src/cli.ts +347 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +62 -0
- package/src/data-transformer.ts +569 -0
- package/src/databasePoolManager.ts +84 -0
- package/src/history/HistoryService.ts +257 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +146 -0
- package/src/schema/generate-drizzle-schema-logic.ts +618 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1447 -0
- package/src/services/EntityPersistService.ts +351 -0
- package/src/services/RelationService.ts +1012 -0
- package/src/services/entity-helpers.ts +121 -0
- package/src/services/entityService.ts +209 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1005 -0
- package/src/utils/drizzle-conditions.ts +999 -0
- package/src/websocket.ts +487 -0
- package/test/auth-services.test.ts +569 -0
- package/test/branchService.test.ts +357 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +352 -0
- package/test/entityService.relations.test.ts +912 -0
- package/test/entityService.subcollection-search.test.ts +516 -0
- package/test/entityService.test.ts +977 -0
- package/test/generate-drizzle-schema.test.ts +795 -0
- package/test/historyService.test.ts +126 -0
- package/test/postgresDataDriver.test.ts +556 -0
- package/test/realtimeService.test.ts +276 -0
- package/test/relations.test.ts +662 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +30 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +82 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { promises as fsPromises } from "fs";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import chokidar from "chokidar";
|
|
6
|
+
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
8
|
+
|
|
9
|
+
// --- Helper Functions ---
|
|
10
|
+
|
|
11
|
+
const formatTerminalText = (text: string, options: {
|
|
12
|
+
bold?: boolean;
|
|
13
|
+
backgroundColor?: "blue" | "green" | "red" | "yellow" | "cyan" | "magenta";
|
|
14
|
+
textColor?: "white" | "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan";
|
|
15
|
+
} = {}): string => {
|
|
16
|
+
let codes = "";
|
|
17
|
+
if (options.bold) codes += "\x1b[1m";
|
|
18
|
+
if (options.backgroundColor) {
|
|
19
|
+
const bgColors = {
|
|
20
|
+
blue: "\x1b[44m",
|
|
21
|
+
green: "\x1b[42m",
|
|
22
|
+
red: "\x1b[41m",
|
|
23
|
+
yellow: "\x1b[43m",
|
|
24
|
+
cyan: "\x1b[46m",
|
|
25
|
+
magenta: "\x1b[45m"
|
|
26
|
+
} as const;
|
|
27
|
+
codes += bgColors[options.backgroundColor];
|
|
28
|
+
}
|
|
29
|
+
if (options.textColor) {
|
|
30
|
+
const textColors = {
|
|
31
|
+
white: "\x1b[37m",
|
|
32
|
+
black: "\x1b[30m",
|
|
33
|
+
red: "\x1b[31m",
|
|
34
|
+
green: "\x1b[32m",
|
|
35
|
+
yellow: "\x1b[33m",
|
|
36
|
+
blue: "\x1b[34m",
|
|
37
|
+
magenta: "\x1b[35m",
|
|
38
|
+
cyan: "\x1b[36m"
|
|
39
|
+
} as const;
|
|
40
|
+
codes += textColors[options.textColor];
|
|
41
|
+
}
|
|
42
|
+
return `${codes}${text}\x1b[0m`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// --- Execution and Watch Logic ---
|
|
46
|
+
|
|
47
|
+
const runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {
|
|
48
|
+
try {
|
|
49
|
+
if (!collectionsFilePath) {
|
|
50
|
+
console.error("Error: No collections file path provided. Skipping schema generation.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resolvedPath = path.resolve(collectionsFilePath);
|
|
55
|
+
let collections: EntityCollection[] = [];
|
|
56
|
+
const stats = fs.statSync(resolvedPath);
|
|
57
|
+
|
|
58
|
+
if (stats.isDirectory()) {
|
|
59
|
+
const files = fs.readdirSync(resolvedPath);
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
if ((file.endsWith('.ts') || file.endsWith('.js')) &&
|
|
62
|
+
!file.includes('.test.') &&
|
|
63
|
+
!file.endsWith('.d.ts') &&
|
|
64
|
+
file !== 'index.ts' && file !== 'index.js') {
|
|
65
|
+
|
|
66
|
+
const filePath = path.join(resolvedPath, file);
|
|
67
|
+
try {
|
|
68
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
69
|
+
const dynamicImport = new Function('url', 'return import(url)');
|
|
70
|
+
const module = await dynamicImport(fileUrl);
|
|
71
|
+
if (module && module.default) {
|
|
72
|
+
collections.push(module.default);
|
|
73
|
+
}
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
console.error(`Error loading ${file}:`, message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
|
|
82
|
+
const dynamicImport = new Function('url', 'return import(url)');
|
|
83
|
+
const imported = await dynamicImport(fileUrl);
|
|
84
|
+
collections = imported.backendCollections || imported.collections;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!collections || !Array.isArray(collections) || collections.length === 0) {
|
|
88
|
+
console.error("Error: Could not find collections array or failed to load directory.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const schemaContent = await generateSchema(collections);
|
|
93
|
+
|
|
94
|
+
if (outputPath) {
|
|
95
|
+
const outputDir = path.dirname(outputPath);
|
|
96
|
+
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
97
|
+
await fsPromises.writeFile(outputPath, schemaContent);
|
|
98
|
+
console.log("✅ Drizzle schema generated successfully at", outputPath);
|
|
99
|
+
} else {
|
|
100
|
+
console.log("✅ Drizzle schema generated successfully.");
|
|
101
|
+
console.log(schemaContent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`You can now run ${formatTerminalText("rebase db generate", {
|
|
105
|
+
bold: true,
|
|
106
|
+
backgroundColor: "blue",
|
|
107
|
+
textColor: "black"
|
|
108
|
+
})} to generate the SQL migration files.`);
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error("Error generating schema:", error);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const main = () => {
|
|
116
|
+
const collectionsFilePathArg = process.argv.find(arg => arg.startsWith("--collections="));
|
|
117
|
+
const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split("=")[1] : process.argv[2];
|
|
118
|
+
|
|
119
|
+
const outputPathArg = process.argv.find(arg => arg.startsWith("--output="));
|
|
120
|
+
const outputPath = outputPathArg ? outputPathArg.split("=")[1] : undefined;
|
|
121
|
+
|
|
122
|
+
const watch = process.argv.includes("--watch");
|
|
123
|
+
|
|
124
|
+
if (!collectionsFilePath) {
|
|
125
|
+
console.log("Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);
|
|
130
|
+
const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;
|
|
131
|
+
|
|
132
|
+
if (watch) {
|
|
133
|
+
console.log(`Watching for changes in ${resolvedPath}...`);
|
|
134
|
+
const watcher = chokidar.watch(resolvedPath, {
|
|
135
|
+
persistent: true,
|
|
136
|
+
ignoreInitial: false
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
watcher.on("all", (event, filePath) => {
|
|
140
|
+
console.log(`[${event}] ${filePath}. Regenerating schema...`);
|
|
141
|
+
runGeneration(resolvedPath, resolvedOutputPath);
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
runGeneration(resolvedPath, resolvedOutputPath);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// This check ensures the script only runs when executed directly
|
|
149
|
+
if (import.meta.url.endsWith(process.argv[1])) {
|
|
150
|
+
main();
|
|
151
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BranchService
|
|
3
|
+
*
|
|
4
|
+
* Manages database branching by creating/deleting PostgreSQL databases
|
|
5
|
+
* using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the
|
|
6
|
+
* `rebase.branches` table in the default (main) database, following the
|
|
7
|
+
* same `rebase` schema convention used by entity_history, auth, etc.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { sql } from "drizzle-orm";
|
|
11
|
+
import { BranchInfo } from "@rebasepro/types";
|
|
12
|
+
import { DrizzleClient } from "../interfaces";
|
|
13
|
+
import { DatabasePoolManager } from "../databasePoolManager";
|
|
14
|
+
|
|
15
|
+
/** Internal prefix applied to branch database names to avoid collisions. */
|
|
16
|
+
const BRANCH_DB_PREFIX = "rb_";
|
|
17
|
+
|
|
18
|
+
/** Fully-qualified metadata table in the rebase schema. */
|
|
19
|
+
const BRANCHES_TABLE = "rebase.branches";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize a user-provided branch name to a safe PostgreSQL identifier.
|
|
23
|
+
* Only allows alphanumeric characters and underscores.
|
|
24
|
+
*/
|
|
25
|
+
function sanitizeBranchName(name: string): string {
|
|
26
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Convert a user-facing branch name to the actual PostgreSQL database name.
|
|
31
|
+
*/
|
|
32
|
+
function toBranchDbName(name: string): string {
|
|
33
|
+
const sanitized = sanitizeBranchName(name);
|
|
34
|
+
if (!sanitized) throw new Error("Branch name must contain at least one alphanumeric character.");
|
|
35
|
+
return `${BRANCH_DB_PREFIX}${sanitized}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class BranchService {
|
|
39
|
+
constructor(
|
|
40
|
+
private db: DrizzleClient,
|
|
41
|
+
private poolManager: DatabasePoolManager
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ensure the `rebase.branches` metadata table exists in the default database.
|
|
46
|
+
* Idempotent — safe to call on every startup.
|
|
47
|
+
*/
|
|
48
|
+
async ensureBranchMetadataTable(): Promise<void> {
|
|
49
|
+
// Create the rebase schema (idempotent — may already exist from auth/history init)
|
|
50
|
+
await this.db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
51
|
+
|
|
52
|
+
await this.db.execute(sql.raw(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (
|
|
54
|
+
name TEXT PRIMARY KEY,
|
|
55
|
+
db_name TEXT NOT NULL UNIQUE,
|
|
56
|
+
parent_db TEXT NOT NULL,
|
|
57
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
58
|
+
metadata JSONB DEFAULT '{}'
|
|
59
|
+
);
|
|
60
|
+
`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a new branch database by templating the source database.
|
|
65
|
+
*
|
|
66
|
+
* Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy
|
|
67
|
+
* of both schema and data.
|
|
68
|
+
*
|
|
69
|
+
* @param name User-facing branch name (e.g., "feature_auth")
|
|
70
|
+
* @param options.source Source database to clone; defaults to the main database.
|
|
71
|
+
*/
|
|
72
|
+
async createBranch(name: string, options?: { source?: string }): Promise<BranchInfo> {
|
|
73
|
+
const dbName = toBranchDbName(name);
|
|
74
|
+
const sanitizedName = sanitizeBranchName(name);
|
|
75
|
+
const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
|
|
76
|
+
|
|
77
|
+
// Check if branch already exists
|
|
78
|
+
const existing = await this.db.execute(
|
|
79
|
+
sql`SELECT name FROM rebase.branches WHERE name = ${sanitizedName} OR db_name = ${dbName}`
|
|
80
|
+
);
|
|
81
|
+
if ((existing.rows as unknown[]).length > 0) {
|
|
82
|
+
throw new Error(`Branch "${sanitizedName}" already exists.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Disconnect any idle pools to the source DB so TEMPLATE works.
|
|
86
|
+
// CREATE DATABASE ... TEMPLATE requires no other connections to the template.
|
|
87
|
+
await this.poolManager.disconnectDatabase(sourceDb);
|
|
88
|
+
|
|
89
|
+
// Create the database using the source as a template.
|
|
90
|
+
// Note: Identifiers must be double-quoted, not parameterized.
|
|
91
|
+
const safeDbName = dbName.replace(/"/g, '""');
|
|
92
|
+
const safeSourceDb = sourceDb.replace(/"/g, '""');
|
|
93
|
+
try {
|
|
94
|
+
await this.db.execute(
|
|
95
|
+
sql.raw(`CREATE DATABASE "${safeDbName}" TEMPLATE "${safeSourceDb}"`)
|
|
96
|
+
);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
if (msg.includes("already exists")) {
|
|
100
|
+
throw new Error(`Database "${dbName}" already exists on the server. Choose a different branch name.`);
|
|
101
|
+
}
|
|
102
|
+
// If template fails due to active connections, provide a helpful error
|
|
103
|
+
if (msg.includes("being accessed by other users")) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Cannot create branch: the source database "${sourceDb}" has active connections. ` +
|
|
106
|
+
`Close other clients or connections and try again.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Record metadata in the default database
|
|
113
|
+
const now = new Date();
|
|
114
|
+
await this.db.execute(
|
|
115
|
+
sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
|
|
116
|
+
VALUES (${sanitizedName}, ${dbName}, ${sourceDb}, ${now.toISOString()})`
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
name: sanitizedName,
|
|
121
|
+
parentDatabase: sourceDb,
|
|
122
|
+
createdAt: now,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Delete a branch database and remove its metadata.
|
|
128
|
+
* Cannot delete the main/default database.
|
|
129
|
+
*/
|
|
130
|
+
async deleteBranch(name: string): Promise<void> {
|
|
131
|
+
const sanitizedName = sanitizeBranchName(name);
|
|
132
|
+
const dbName = toBranchDbName(name);
|
|
133
|
+
|
|
134
|
+
// Safety: never delete the default database
|
|
135
|
+
if (dbName === this.poolManager.defaultDatabaseName) {
|
|
136
|
+
throw new Error("Cannot delete the main database.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Verify the branch exists in our metadata
|
|
140
|
+
const existing = await this.db.execute(
|
|
141
|
+
sql`SELECT db_name FROM rebase.branches WHERE name = ${sanitizedName}`
|
|
142
|
+
);
|
|
143
|
+
if ((existing.rows as unknown[]).length === 0) {
|
|
144
|
+
throw new Error(`Branch "${sanitizedName}" not found.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Disconnect any pools to this branch before dropping
|
|
148
|
+
await this.poolManager.disconnectDatabase(dbName);
|
|
149
|
+
|
|
150
|
+
// Drop the database
|
|
151
|
+
const safeDbName = dbName.replace(/"/g, '""');
|
|
152
|
+
try {
|
|
153
|
+
await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
156
|
+
if (msg.includes("being accessed by other users")) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Cannot delete branch "${sanitizedName}": the database has active connections. ` +
|
|
159
|
+
`Close other clients and try again.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Remove metadata
|
|
166
|
+
await this.db.execute(
|
|
167
|
+
sql`DELETE FROM rebase.branches WHERE name = ${sanitizedName}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* List all branches recorded in the metadata table.
|
|
173
|
+
* Optionally fetches database sizes from pg_database.
|
|
174
|
+
*/
|
|
175
|
+
async listBranches(): Promise<BranchInfo[]> {
|
|
176
|
+
const result = await this.db.execute(sql.raw(`
|
|
177
|
+
SELECT
|
|
178
|
+
b.name,
|
|
179
|
+
b.parent_db,
|
|
180
|
+
b.created_at,
|
|
181
|
+
pg_database_size(b.db_name) as size_bytes
|
|
182
|
+
FROM ${BRANCHES_TABLE} b
|
|
183
|
+
JOIN pg_database d ON d.datname = b.db_name
|
|
184
|
+
ORDER BY b.created_at DESC
|
|
185
|
+
`));
|
|
186
|
+
|
|
187
|
+
return (result.rows as Record<string, unknown>[]).map((row) => ({
|
|
188
|
+
name: row.name as string,
|
|
189
|
+
parentDatabase: row.parent_db as string,
|
|
190
|
+
createdAt: new Date(row.created_at as string),
|
|
191
|
+
sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : undefined,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get info about a specific branch.
|
|
197
|
+
*/
|
|
198
|
+
async getBranchInfo(name: string): Promise<BranchInfo | undefined> {
|
|
199
|
+
const sanitizedName = sanitizeBranchName(name);
|
|
200
|
+
|
|
201
|
+
const result = await this.db.execute(sql`
|
|
202
|
+
SELECT
|
|
203
|
+
b.name,
|
|
204
|
+
b.parent_db,
|
|
205
|
+
b.created_at
|
|
206
|
+
FROM rebase.branches b
|
|
207
|
+
WHERE b.name = ${sanitizedName}
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
const rows = result.rows as Record<string, unknown>[];
|
|
211
|
+
if (rows.length === 0) return undefined;
|
|
212
|
+
|
|
213
|
+
const row = rows[0];
|
|
214
|
+
|
|
215
|
+
// Attempt to get size — may fail if the DB was externally dropped
|
|
216
|
+
let sizeBytes: number | undefined;
|
|
217
|
+
try {
|
|
218
|
+
const dbName = toBranchDbName(sanitizedName);
|
|
219
|
+
const sizeResult = await this.db.execute(
|
|
220
|
+
sql`SELECT pg_database_size(${dbName}) as size_bytes`
|
|
221
|
+
);
|
|
222
|
+
const sizeRows = sizeResult.rows as Record<string, unknown>[];
|
|
223
|
+
if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) {
|
|
224
|
+
sizeBytes = Number(sizeRows[0].size_bytes);
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Database might not exist anymore
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
name: row.name as string,
|
|
232
|
+
parentDatabase: row.parent_db as string,
|
|
233
|
+
createdAt: new Date(row.created_at as string),
|
|
234
|
+
sizeBytes,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|