@rebasepro/server-postgresql 0.0.1-canary.09e5ec5
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 +56 -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 +58 -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 +22 -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 +11298 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +11306 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -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 +192 -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 +40 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +58 -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 +868 -0
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -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/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -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 +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -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/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -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 +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -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 +10 -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 +23 -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 +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -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 +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -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/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +92 -0
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +1008 -0
- package/src/PostgresBootstrapper.ts +231 -0
- package/src/auth/ensure-tables.ts +381 -0
- package/src/auth/services.ts +799 -0
- package/src/cli.ts +648 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +84 -0
- package/src/data-transformer.ts +608 -0
- package/src/databasePoolManager.ts +85 -0
- package/src/history/HistoryService.ts +248 -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 +169 -0
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +765 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/schema/introspect-db-logic.ts +542 -0
- package/src/schema/introspect-db.ts +211 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1576 -0
- package/src/services/EntityPersistService.ts +349 -0
- package/src/services/RelationService.ts +1274 -0
- package/src/services/entity-helpers.ts +147 -0
- package/src/services/entityService.ts +211 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1034 -0
- package/src/utils/drizzle-conditions.ts +1000 -0
- package/src/websocket.ts +518 -0
- package/test/auth-services.test.ts +661 -0
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +367 -0
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +367 -0
- package/test/entityService.relations.test.ts +1008 -0
- package/test/entityService.subcollection-search.test.ts +566 -0
- package/test/entityService.test.ts +1035 -0
- package/test/generate-drizzle-schema.test.ts +988 -0
- package/test/historyService.test.ts +141 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +389 -0
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +648 -0
- package/test/realtimeService.test.ts +307 -0
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +1115 -0
- package/test/unmapped-tables-safety.test.ts +345 -0
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +32 -0
- package/test_hash.js +14 -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,211 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import pg from "pg";
|
|
5
|
+
import arg from "arg";
|
|
6
|
+
import * as dotenv from "dotenv";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
TableRow,
|
|
10
|
+
TableColumn,
|
|
11
|
+
EnumValue,
|
|
12
|
+
PrimaryKeyRow,
|
|
13
|
+
ForeignKeyRow,
|
|
14
|
+
buildTablesMap,
|
|
15
|
+
buildEnumMap,
|
|
16
|
+
identifyJoinTables,
|
|
17
|
+
generateCollectionFile,
|
|
18
|
+
generateIndexContent,
|
|
19
|
+
mergeIndexContent,
|
|
20
|
+
safeHostFromUrl,
|
|
21
|
+
} from "./introspect-db-logic";
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const args = arg(
|
|
25
|
+
{
|
|
26
|
+
"--output": String,
|
|
27
|
+
"--force": Boolean,
|
|
28
|
+
"--schema": String,
|
|
29
|
+
"-o": "--output",
|
|
30
|
+
"-f": "--force",
|
|
31
|
+
},
|
|
32
|
+
{ permissive: true }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const outDir = args["--output"] || path.resolve(process.cwd(), "config", "collections");
|
|
36
|
+
const force = args["--force"] || false;
|
|
37
|
+
const pgSchema = args["--schema"] || "public";
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(outDir)) {
|
|
40
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load env
|
|
44
|
+
const envPaths = [
|
|
45
|
+
process.env.DOTENV_CONFIG_PATH,
|
|
46
|
+
path.resolve(process.cwd(), ".env"),
|
|
47
|
+
path.resolve(process.cwd(), "../.env"),
|
|
48
|
+
path.resolve(process.cwd(), "../../.env")
|
|
49
|
+
].filter(Boolean) as string[];
|
|
50
|
+
|
|
51
|
+
for (const p of envPaths) {
|
|
52
|
+
if (fs.existsSync(p)) {
|
|
53
|
+
dotenv.config({ path: p });
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
59
|
+
if (!databaseUrl) {
|
|
60
|
+
console.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const client = new pg.Client({ connectionString: databaseUrl });
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await client.connect();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
|
|
70
|
+
console.error(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Log the host portion safely — handle URLs without "@"
|
|
75
|
+
const hostPart = safeHostFromUrl(databaseUrl);
|
|
76
|
+
console.log(chalk.gray(`Connected to database: ${hostPart}`));
|
|
77
|
+
console.log(chalk.gray(`Introspecting schema '${pgSchema}'...`));
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// 1. Get Tables
|
|
81
|
+
const { rows: tables } = await client.query<TableRow>(`
|
|
82
|
+
SELECT table_name
|
|
83
|
+
FROM information_schema.tables
|
|
84
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
85
|
+
AND table_name NOT LIKE 'drizzle_%'
|
|
86
|
+
AND table_name NOT LIKE 'rebase_%'
|
|
87
|
+
ORDER BY table_name
|
|
88
|
+
`, [pgSchema]);
|
|
89
|
+
|
|
90
|
+
// 2. Get Columns
|
|
91
|
+
const { rows: columns } = await client.query<TableColumn>(`
|
|
92
|
+
SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default
|
|
93
|
+
FROM information_schema.columns
|
|
94
|
+
WHERE table_schema = $1
|
|
95
|
+
`, [pgSchema]);
|
|
96
|
+
|
|
97
|
+
// 2b. Get Enum Types and their values
|
|
98
|
+
const { rows: enumValues } = await client.query<EnumValue>(`
|
|
99
|
+
SELECT t.typname AS enum_name,
|
|
100
|
+
e.enumlabel AS enum_value,
|
|
101
|
+
e.enumsortorder AS sort_order
|
|
102
|
+
FROM pg_type t
|
|
103
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
104
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
105
|
+
WHERE n.nspname = $1
|
|
106
|
+
ORDER BY t.typname, e.enumsortorder
|
|
107
|
+
`, [pgSchema]);
|
|
108
|
+
|
|
109
|
+
// Build a map: enum_name -> ordered list of values
|
|
110
|
+
const enumMap = buildEnumMap(enumValues);
|
|
111
|
+
|
|
112
|
+
// 3. Get Primary Keys
|
|
113
|
+
const { rows: pks } = await client.query<PrimaryKeyRow>(`
|
|
114
|
+
SELECT t.relname as table_name, a.attname as column_name
|
|
115
|
+
FROM pg_index i
|
|
116
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
117
|
+
AND a.attnum = ANY(i.indkey)
|
|
118
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
119
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
120
|
+
WHERE i.indisprimary AND n.nspname = $1
|
|
121
|
+
`, [pgSchema]);
|
|
122
|
+
|
|
123
|
+
// 4. Get Foreign Keys
|
|
124
|
+
const { rows: fks } = await client.query<ForeignKeyRow>(`
|
|
125
|
+
SELECT
|
|
126
|
+
tc.table_name,
|
|
127
|
+
kcu.column_name,
|
|
128
|
+
ccu.table_name AS foreign_table_name,
|
|
129
|
+
ccu.column_name AS foreign_column_name
|
|
130
|
+
FROM
|
|
131
|
+
information_schema.table_constraints AS tc
|
|
132
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
133
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
134
|
+
AND tc.table_schema = kcu.table_schema
|
|
135
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
136
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
137
|
+
AND ccu.table_schema = tc.table_schema
|
|
138
|
+
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1
|
|
139
|
+
`, [pgSchema]);
|
|
140
|
+
|
|
141
|
+
const tablesMap = buildTablesMap(tables, columns, pks, fks);
|
|
142
|
+
const joinTables = identifyJoinTables(tablesMap);
|
|
143
|
+
|
|
144
|
+
console.log(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
|
|
145
|
+
|
|
146
|
+
// Generate Collections
|
|
147
|
+
const generatedFiles: string[] = [];
|
|
148
|
+
const skippedFiles: string[] = [];
|
|
149
|
+
|
|
150
|
+
for (const [tableName, meta] of tablesMap.entries()) {
|
|
151
|
+
if (joinTables.has(tableName)) continue; // We don't generate base collections for pure join tables
|
|
152
|
+
|
|
153
|
+
// ── File overwrite protection ──────────────────────────────
|
|
154
|
+
const filePath = path.join(outDir, `${tableName}.ts`);
|
|
155
|
+
if (fs.existsSync(filePath) && !force) {
|
|
156
|
+
skippedFiles.push(tableName);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fileContent = generateCollectionFile(
|
|
161
|
+
tableName,
|
|
162
|
+
meta,
|
|
163
|
+
fks,
|
|
164
|
+
joinTables,
|
|
165
|
+
tablesMap,
|
|
166
|
+
enumMap,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
170
|
+
generatedFiles.push(tableName);
|
|
171
|
+
console.log(chalk.green(` ✓ ${filePath}`));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Generate index.ts (sorted alphabetically for deterministic output)
|
|
175
|
+
if (generatedFiles.length > 0) {
|
|
176
|
+
const indexPath = path.join(outDir, "index.ts");
|
|
177
|
+
|
|
178
|
+
if (fs.existsSync(indexPath) && !force) {
|
|
179
|
+
// Merge: read existing index, add new exports that don't already exist
|
|
180
|
+
const existing = fs.readFileSync(indexPath, "utf-8");
|
|
181
|
+
const merged = mergeIndexContent(existing, generatedFiles);
|
|
182
|
+
fs.writeFileSync(indexPath, merged, "utf-8");
|
|
183
|
+
} else {
|
|
184
|
+
const indexContent = generateIndexContent(generatedFiles);
|
|
185
|
+
fs.writeFileSync(indexPath, indexContent, "utf-8");
|
|
186
|
+
}
|
|
187
|
+
console.log(chalk.green(` ✓ ${indexPath}`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log("");
|
|
191
|
+
if (skippedFiles.length > 0) {
|
|
192
|
+
console.log(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
|
|
193
|
+
console.log(chalk.gray(` Use --force to overwrite existing files.`));
|
|
194
|
+
console.log("");
|
|
195
|
+
}
|
|
196
|
+
console.log(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
|
|
197
|
+
console.log(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
|
|
198
|
+
console.log("");
|
|
199
|
+
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
} finally {
|
|
204
|
+
await client.end();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main().catch((err) => {
|
|
209
|
+
console.error(err);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pgTable, text, pgPolicy } from "drizzle-orm/pg-core";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
export const testTable = pgTable("test", {
|
|
5
|
+
id: text("id").primaryKey()
|
|
6
|
+
}, (t) => [
|
|
7
|
+
pgPolicy("renamed_policy", { as: "permissive",
|
|
8
|
+
to: "public",
|
|
9
|
+
for: "select",
|
|
10
|
+
using: sql`true` })
|
|
11
|
+
]);
|
|
@@ -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
|
+
}
|